summaryrefslogtreecommitdiffstats
path: root/nova/auth
diff options
context:
space:
mode:
authorJesse Andrews <anotherjesse@gmail.com>2010-05-27 23:05:26 -0700
committerJesse Andrews <anotherjesse@gmail.com>2010-05-27 23:05:26 -0700
commitbf6e6e718cdc7488e2da87b21e258ccc065fe499 (patch)
tree51cf4f72047eb6b16079c7fe21e9822895541801 /nova/auth
downloadnova-bf6e6e718cdc7488e2da87b21e258ccc065fe499.tar.gz
nova-bf6e6e718cdc7488e2da87b21e258ccc065fe499.tar.xz
nova-bf6e6e718cdc7488e2da87b21e258ccc065fe499.zip
initial commit
Diffstat (limited to 'nova/auth')
-rw-r--r--nova/auth/__init__.py25
-rw-r--r--nova/auth/access.py69
-rw-r--r--nova/auth/fakeldap.py81
-rw-r--r--nova/auth/novarc.template26
-rw-r--r--nova/auth/rbac.ldif60
-rw-r--r--nova/auth/signer.py127
-rwxr-xr-xnova/auth/slap.sh226
-rwxr-xr-xnova/auth/users.py454
8 files changed, 1068 insertions, 0 deletions
diff --git a/nova/auth/__init__.py b/nova/auth/__init__.py
new file mode 100644
index 000000000..7cd6c618d
--- /dev/null
+++ b/nova/auth/__init__.py
@@ -0,0 +1,25 @@
+# Copyright [2010] [Anso Labs, LLC]
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+:mod:`nova.auth` -- Authentication and Access Control
+=====================================================
+
+.. automodule:: nova.auth
+ :platform: Unix
+ :synopsis: User-and-Project based RBAC using LDAP, SAML.
+.. moduleauthor:: Jesse Andrews <jesse@ansolabs.com>
+.. moduleauthor:: Vishvananda Ishaya <vishvananda@yahoo.com>
+.. moduleauthor:: Joshua McKenty <joshua@cognition.ca>
+""" \ No newline at end of file
diff --git a/nova/auth/access.py b/nova/auth/access.py
new file mode 100644
index 000000000..2c780626d
--- /dev/null
+++ b/nova/auth/access.py
@@ -0,0 +1,69 @@
+# Copyright [2010] [Anso Labs, LLC]
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Simple base set of RBAC rules which map API endpoints to LDAP groups.
+For testing accounts, users will always have PM privileges.
+"""
+
+
+# This is logically a RuleSet or some such.
+
+def allow_describe_images(user, project, target_object):
+ return True
+
+def allow_describe_instances(user, project, target_object):
+ return True
+
+def allow_describe_addresses(user, project, target_object):
+ return True
+
+def allow_run_instances(user, project, target_object):
+ # target_object is a reservation, not an instance
+ # it needs to include count, type, image, etc.
+
+ # First, is the project allowed to use this image
+
+ # Second, is this user allowed to launch within this project
+
+ # Third, is the count or type within project quota
+
+ return True
+
+def allow_terminate_instances(user, project, target_object):
+ # In a project, the PMs and Sysadmins can terminate
+ return True
+
+def allow_get_console_output(user, project, target_object):
+ # If the user launched the instance,
+ # Or is a sysadmin in the project,
+ return True
+
+def allow_allocate_address(user, project, target_object):
+ # There's no security concern in allocation,
+ # but it can get expensive. Limit to PM and NE.
+ return True
+
+def allow_associate_address(user, project, target_object):
+ # project NE only
+ # In future, will perform a CloudAudit scan first
+ # (Pass / Fail gate)
+ return True
+
+def allow_register(user, project, target_object):
+ return False
+
+def is_allowed(action, user, project, target_object):
+ return globals()['allow_%s' % action](user, project, target_object)
+
diff --git a/nova/auth/fakeldap.py b/nova/auth/fakeldap.py
new file mode 100644
index 000000000..c223b250c
--- /dev/null
+++ b/nova/auth/fakeldap.py
@@ -0,0 +1,81 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+# Copyright [2010] [Anso Labs, LLC]
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+ Fake LDAP server for test harnesses.
+"""
+
+import logging
+
+from nova import datastore
+
+SCOPE_SUBTREE = 1
+
+
+class NO_SUCH_OBJECT(Exception):
+ pass
+
+
+def initialize(uri):
+ return FakeLDAP(uri)
+
+
+class FakeLDAP(object):
+ def __init__(self, _uri):
+ self.keeper = datastore.Keeper('fakeldap')
+ if self.keeper['objects'] is None:
+ self.keeper['objects'] = {}
+
+ def simple_bind_s(self, dn, password):
+ pass
+
+ def unbind_s(self):
+ pass
+
+ def search_s(self, dn, scope, query=None, fields=None):
+ logging.debug("searching for %s" % dn)
+ filtered = {}
+ d = self.keeper['objects'] or {}
+ for cn, attrs in d.iteritems():
+ if cn[-len(dn):] == dn:
+ filtered[cn] = attrs
+ if query:
+ k,v = query[1:-1].split('=')
+ objects = {}
+ for cn, attrs in filtered.iteritems():
+ if attrs.has_key(k) and (v in attrs[k] or
+ v == attrs[k]):
+ objects[cn] = attrs
+ if objects == {}:
+ raise NO_SUCH_OBJECT()
+ return objects.items()
+
+ def add_s(self, cn, attr):
+ logging.debug("adding %s" % cn)
+ stored = {}
+ for k, v in attr:
+ if type(v) is list:
+ stored[k] = v
+ else:
+ stored[k] = [v]
+ d = self.keeper['objects']
+ d[cn] = stored
+ self.keeper['objects'] = d
+
+ def delete_s(self, cn):
+ logging.debug("creating for %s" % cn)
+ d = self.keeper['objects'] or {}
+ del d[cn]
+ self.keeper['objects'] = d
diff --git a/nova/auth/novarc.template b/nova/auth/novarc.template
new file mode 100644
index 000000000..a993d1882
--- /dev/null
+++ b/nova/auth/novarc.template
@@ -0,0 +1,26 @@
+# Copyright [2010] [Anso Labs, LLC]
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+NOVA_KEY_DIR=$(pushd $(dirname $BASH_SOURCE)>/dev/null; pwd; popd>/dev/null)
+export EC2_ACCESS_KEY="%(access)s"
+export EC2_SECRET_KEY="%(secret)s"
+export EC2_URL="%(ec2)s"
+export S3_URL="%(s3)s"
+export EC2_USER_ID=42 # nova does not use user id, but bundling requires it
+export EC2_PRIVATE_KEY=${NOVA_KEY_DIR}/%(key)s
+export EC2_CERT=${NOVA_KEY_DIR}/%(cert)s
+export NOVA_CERT=${NOVA_KEY_DIR}/%(nova)s
+export EUCALYPTUS_CERT=${NOVA_CERT} # euca-bundle-image seems to require this set
+alias ec2-bundle-image="ec2-bundle-image --cert ${EC2_CERT} --privatekey ${EC2_PRIVATE_KEY} --user 42 --ec2cert ${NOVA_CERT}"
+alias ec2-upload-bundle="ec2-upload-bundle -a ${EC2_ACCESS_KEY} -s ${EC2_SECRET_KEY} --url ${S3_URL} --ec2cert ${NOVA_CERT}"
diff --git a/nova/auth/rbac.ldif b/nova/auth/rbac.ldif
new file mode 100644
index 000000000..3878d2c1b
--- /dev/null
+++ b/nova/auth/rbac.ldif
@@ -0,0 +1,60 @@
+# Copyright [2010] [Anso Labs, LLC]
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# LDIF fragment to create group branch under root
+
+#dn: ou=Groups,dc=example,dc=com
+#objectclass:organizationalunit
+#ou: groups
+#description: generic groups branch
+
+# create the itpeople entry
+
+dn: cn=sysadmins,ou=Groups,dc=example,dc=com
+objectclass: groupofnames
+cn: itpeople
+description: IT admin group
+# add the group members all of which are
+# assumed to exist under Users
+#member: cn=micky mouse,ou=people,dc=example,dc=com
+member: cn=admin,ou=Users,dc=example,dc=com
+
+dn: cn=netadmins,ou=Groups,dc=example,dc=com
+objectclass: groupofnames
+cn: netadmins
+description: Network admin group
+member: cn=admin,ou=Users,dc=example,dc=com
+
+dn: cn=cloudadmins,ou=Groups,dc=example,dc=com
+objectclass: groupofnames
+cn: cloudadmins
+description: Cloud admin group
+member: cn=admin,ou=Users,dc=example,dc=com
+
+dn: cn=itsec,ou=Groups,dc=example,dc=com
+objectclass: groupofnames
+cn: itsec
+description: IT security users group
+member: cn=admin,ou=Users,dc=example,dc=com
+
+# Example Project Group to demonstrate members
+# and project members
+
+dn: cn=myproject,ou=Groups,dc=example,dc=com
+objectclass: groupofnames
+objectclass: novaProject
+cn: myproject
+description: My Project Group
+member: cn=admin,ou=Users,dc=example,dc=com
+projectManager: cn=admin,ou=Users,dc=example,dc=com
diff --git a/nova/auth/signer.py b/nova/auth/signer.py
new file mode 100644
index 000000000..00aa066fb
--- /dev/null
+++ b/nova/auth/signer.py
@@ -0,0 +1,127 @@
+# Copyright [2010] [Anso Labs, LLC]
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# PORTIONS OF THIS FILE ARE FROM:
+# http://code.google.com/p/boto
+# Copyright (c) 2006-2009 Mitch Garnaat http://garnaat.org/
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish, dis-
+# tribute, sublicense, and/or sell copies of the Software, and to permit
+# persons to whom the Software is furnished to do so, subject to the fol-
+# lowing conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
+# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+"""
+Utility class for parsing signed AMI manifests.
+"""
+
+import logging
+import hashlib
+import hmac
+import urllib
+import base64
+from nova.exception import Error
+
+_log = logging.getLogger('signer')
+logging.getLogger('signer').setLevel(logging.WARN)
+
+class Signer(object):
+ """ hacked up code from boto/connection.py """
+
+ def __init__(self, secret_key):
+ self.hmac = hmac.new(secret_key, digestmod=hashlib.sha1)
+ if hashlib.sha256:
+ self.hmac_256 = hmac.new(secret_key, digestmod=hashlib.sha256)
+
+ def generate(self, params, verb, server_string, path):
+ if params['SignatureVersion'] == '0':
+ t = self._calc_signature_0(params)
+ elif params['SignatureVersion'] == '1':
+ t = self._calc_signature_1(params)
+ elif params['SignatureVersion'] == '2':
+ t = self._calc_signature_2(params, verb, server_string, path)
+ else:
+ raise Error('Unknown Signature Version: %s' % self.SignatureVersion)
+ return t
+
+ def _get_utf8_value(self, value):
+ if not isinstance(value, str) and not isinstance(value, unicode):
+ value = str(value)
+ if isinstance(value, unicode):
+ return value.encode('utf-8')
+ else:
+ return value
+
+ def _calc_signature_0(self, params):
+ s = params['Action'] + params['Timestamp']
+ self.hmac.update(s)
+ keys = params.keys()
+ keys.sort(cmp = lambda x, y: cmp(x.lower(), y.lower()))
+ pairs = []
+ for key in keys:
+ val = self._get_utf8_value(params[key])
+ pairs.append(key + '=' + urllib.quote(val))
+ return base64.b64encode(self.hmac.digest())
+
+ def _calc_signature_1(self, params):
+ keys = params.keys()
+ keys.sort(cmp = lambda x, y: cmp(x.lower(), y.lower()))
+ pairs = []
+ for key in keys:
+ self.hmac.update(key)
+ val = self._get_utf8_value(params[key])
+ self.hmac.update(val)
+ pairs.append(key + '=' + urllib.quote(val))
+ return base64.b64encode(self.hmac.digest())
+
+ def _calc_signature_2(self, params, verb, server_string, path):
+ _log.debug('using _calc_signature_2')
+ string_to_sign = '%s\n%s\n%s\n' % (verb, server_string, path)
+ if self.hmac_256:
+ hmac = self.hmac_256
+ params['SignatureMethod'] = 'HmacSHA256'
+ else:
+ hmac = self.hmac
+ params['SignatureMethod'] = 'HmacSHA1'
+ keys = params.keys()
+ keys.sort()
+ pairs = []
+ for key in keys:
+ val = self._get_utf8_value(params[key])
+ pairs.append(urllib.quote(key, safe='') + '=' + urllib.quote(val, safe='-_~'))
+ qs = '&'.join(pairs)
+ _log.debug('query string: %s' % qs)
+ string_to_sign += qs
+ _log.debug('string_to_sign: %s' % string_to_sign)
+ hmac.update(string_to_sign)
+ b64 = base64.b64encode(hmac.digest())
+ _log.debug('len(b64)=%d' % len(b64))
+ _log.debug('base64 encoded digest: %s' % b64)
+ return b64
+
+if __name__ == '__main__':
+ print Signer('foo').generate({"SignatureMethod": 'HmacSHA256', 'SignatureVersion': '2'}, "get", "server", "/foo")
diff --git a/nova/auth/slap.sh b/nova/auth/slap.sh
new file mode 100755
index 000000000..a0df4e0ae
--- /dev/null
+++ b/nova/auth/slap.sh
@@ -0,0 +1,226 @@
+#!/usr/bin/env bash
+# Copyright [2010] [Anso Labs, LLC]
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# LDAP INSTALL SCRIPT - SHOULD BE IDEMPOTENT, but it SCRUBS all USERS
+
+apt-get install -y slapd ldap-utils python-ldap
+
+cat >/etc/ldap/schema/openssh-lpk_openldap.schema <<LPK_SCHEMA_EOF
+#
+# LDAP Public Key Patch schema for use with openssh-ldappubkey
+# Author: Eric AUGE <eau@phear.org>
+#
+# Based on the proposal of : Mark Ruijter
+#
+
+
+# octetString SYNTAX
+attributetype ( 1.3.6.1.4.1.24552.500.1.1.1.13 NAME 'sshPublicKey'
+ DESC 'MANDATORY: OpenSSH Public key'
+ EQUALITY octetStringMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
+
+# printableString SYNTAX yes|no
+objectclass ( 1.3.6.1.4.1.24552.500.1.1.2.0 NAME 'ldapPublicKey' SUP top AUXILIARY
+ DESC 'MANDATORY: OpenSSH LPK objectclass'
+ MAY ( sshPublicKey $ uid )
+ )
+LPK_SCHEMA_EOF
+
+cat >/etc/ldap/schema/nova.schema <<NOVA_SCHEMA_EOF
+#
+# Person object for Nova
+# inetorgperson with extra attributes
+# Author: Vishvananda Ishaya <vishvananda@yahoo.com>
+#
+#
+
+# using internet experimental oid arc as per BP64 3.1
+objectidentifier novaSchema 1.3.6.1.3.1.666.666
+objectidentifier novaAttrs novaSchema:3
+objectidentifier novaOCs novaSchema:4
+
+attributetype (
+ novaAttrs:1
+ NAME 'accessKey'
+ DESC 'Key for accessing data'
+ EQUALITY caseIgnoreMatch
+ SUBSTR caseIgnoreSubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
+ SINGLE-VALUE
+ )
+
+attributetype (
+ novaAttrs:2
+ NAME 'secretKey'
+ DESC 'Secret key'
+ EQUALITY caseIgnoreMatch
+ SUBSTR caseIgnoreSubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
+ SINGLE-VALUE
+ )
+
+attributetype (
+ novaAttrs:3
+ NAME 'keyFingerprint'
+ DESC 'Fingerprint of private key'
+ EQUALITY caseIgnoreMatch
+ SUBSTR caseIgnoreSubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
+ SINGLE-VALUE
+ )
+
+attributetype (
+ novaAttrs:4
+ NAME 'isAdmin'
+ DESC 'Is user an administrator?'
+ EQUALITY booleanMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
+ SINGLE-VALUE
+ )
+
+attributetype (
+ novaAttrs:5
+ NAME 'projectManager'
+ DESC 'Project Managers of a project'
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.12
+ )
+
+objectClass (
+ novaOCs:1
+ NAME 'novaUser'
+ DESC 'access and secret keys'
+ AUXILIARY
+ MUST ( uid )
+ MAY ( accessKey $ secretKey $ isAdmin )
+ )
+
+objectClass (
+ novaOCs:2
+ NAME 'novaKeyPair'
+ DESC 'Key pair for User'
+ SUP top
+ STRUCTURAL
+ MUST ( cn $ sshPublicKey $ keyFingerprint )
+ )
+
+objectClass (
+ novaOCs:3
+ NAME 'novaProject'
+ DESC 'Container for project'
+ SUP groupofnames
+ STRUCTURAL
+ MUST ( cn $ projectManager )
+ )
+
+NOVA_SCHEMA_EOF
+
+mv /etc/ldap/slapd.conf /etc/ldap/slapd.conf.orig
+cat >/etc/ldap/slapd.conf <<SLAPD_CONF_EOF
+# slapd.conf - Configuration file for LDAP SLAPD
+##########
+# Basics #
+##########
+include /etc/ldap/schema/core.schema
+include /etc/ldap/schema/cosine.schema
+include /etc/ldap/schema/inetorgperson.schema
+include /etc/ldap/schema/openssh-lpk_openldap.schema
+include /etc/ldap/schema/nova.schema
+pidfile /var/run/slapd/slapd.pid
+argsfile /var/run/slapd/slapd.args
+loglevel none
+modulepath /usr/lib/ldap
+# modulepath /usr/local/libexec/openldap
+moduleload back_hdb
+##########################
+# Database Configuration #
+##########################
+database hdb
+suffix "dc=example,dc=com"
+rootdn "cn=Manager,dc=example,dc=com"
+rootpw changeme
+directory /var/lib/ldap
+# directory /usr/local/var/openldap-data
+index objectClass,cn eq
+########
+# ACLs #
+########
+access to attrs=userPassword
+ by anonymous auth
+ by self write
+ by * none
+access to *
+ by self write
+ by * none
+SLAPD_CONF_EOF
+
+mv /etc/ldap/ldap.conf /etc/ldap/ldap.conf.orig
+
+cat >/etc/ldap/ldap.conf <<LDAP_CONF_EOF
+# LDAP Client Settings
+URI ldap://localhost
+BASE dc=example,dc=com
+BINDDN cn=Manager,dc=example,dc=com
+SIZELIMIT 0
+TIMELIMIT 0
+LDAP_CONF_EOF
+
+cat >/etc/ldap/base.ldif <<BASE_LDIF_EOF
+# This is the root of the directory tree
+dn: dc=example,dc=com
+description: Example.Com, your trusted non-existent corporation.
+dc: example
+o: Example.Com
+objectClass: top
+objectClass: dcObject
+objectClass: organization
+
+# Subtree for users
+dn: ou=Users,dc=example,dc=com
+ou: Users
+description: Users
+objectClass: organizationalUnit
+
+# Subtree for groups
+dn: ou=Groups,dc=example,dc=com
+ou: Groups
+description: Groups
+objectClass: organizationalUnit
+
+# Subtree for system accounts
+dn: ou=System,dc=example,dc=com
+ou: System
+description: Special accounts used by software applications.
+objectClass: organizationalUnit
+
+# Special Account for Authentication:
+dn: uid=authenticate,ou=System,dc=example,dc=com
+uid: authenticate
+ou: System
+description: Special account for authenticating users
+userPassword: {MD5}TLnIqASP0CKUR3/LGkEZGg==
+objectClass: account
+objectClass: simpleSecurityObject
+BASE_LDIF_EOF
+
+/etc/init.d/slapd stop
+rm -rf /var/lib/ldap/*
+rm -rf /etc/ldap/slapd.d/*
+slaptest -f /etc/ldap/slapd.conf -F /etc/ldap/slapd.d
+cp /usr/share/slapd/DB_CONFIG /var/lib/ldap/DB_CONFIG
+slapadd -v -l /etc/ldap/base.ldif
+chown -R openldap:openldap /etc/ldap/slapd.d
+chown -R openldap:openldap /var/lib/ldap
+/etc/init.d/slapd start
diff --git a/nova/auth/users.py b/nova/auth/users.py
new file mode 100755
index 000000000..d8ea8ac68
--- /dev/null
+++ b/nova/auth/users.py
@@ -0,0 +1,454 @@
+#!/usr/bin/env python
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+# Copyright [2010] [Anso Labs, LLC]
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Nova users and user management, including RBAC hooks.
+"""
+
+import datetime
+import logging
+import os
+import shutil
+import tempfile
+import uuid
+import zipfile
+
+try:
+ import ldap
+except Exception, e:
+ import fakeldap as ldap
+
+import fakeldap
+from nova import datastore
+
+# TODO(termie): clean up these imports
+import signer
+from nova import exception
+from nova import flags
+from nova import crypto
+from nova import utils
+import access as simplerbac
+
+from nova import objectstore # for flags
+
+FLAGS = flags.FLAGS
+
+flags.DEFINE_string('ldap_url', 'ldap://localhost', 'Point this at your ldap server')
+flags.DEFINE_string('ldap_password', 'changeme', 'LDAP password')
+flags.DEFINE_string('user_dn', 'cn=Manager,dc=example,dc=com', 'DN of admin user')
+flags.DEFINE_string('user_unit', 'Users', 'OID for Users')
+flags.DEFINE_string('ldap_subtree', 'ou=Users,dc=example,dc=com', 'OU for Users')
+
+flags.DEFINE_string('ldap_sysadmin',
+ 'cn=sysadmins,ou=Groups,dc=example,dc=com', 'OU for Sysadmins')
+flags.DEFINE_string('ldap_netadmin',
+ 'cn=netadmins,ou=Groups,dc=example,dc=com', 'OU for NetAdmins')
+flags.DEFINE_string('ldap_cloudadmin',
+ 'cn=cloudadmins,ou=Groups,dc=example,dc=com', 'OU for Cloud Admins')
+flags.DEFINE_string('ldap_itsec',
+ 'cn=itsec,ou=Groups,dc=example,dc=com', 'OU for ItSec')
+
+flags.DEFINE_string('credentials_template',
+ utils.abspath('auth/novarc.template'),
+ 'Template for creating users rc file')
+flags.DEFINE_string('credential_key_file', 'pk.pem',
+ 'Filename of private key in credentials zip')
+flags.DEFINE_string('credential_cert_file', 'cert.pem',
+ 'Filename of certificate in credentials zip')
+flags.DEFINE_string('credential_rc_file', 'novarc',
+ 'Filename of rc in credentials zip')
+
+_log = logging.getLogger('auth')
+_log.setLevel(logging.WARN)
+
+
+
+class UserError(exception.ApiError):
+ pass
+
+class InvalidKeyPair(exception.ApiError):
+ pass
+
+class User(object):
+ def __init__(self, id, name, access, secret, admin):
+ self.manager = UserManager.instance()
+ self.id = id
+ self.name = name
+ self.access = access
+ self.secret = secret
+ self.admin = admin
+ self.keeper = datastore.Keeper(prefix="user")
+
+
+ def is_admin(self):
+ return self.admin
+
+ def has_role(self, role_type):
+ return self.manager.has_role(self.id, role_type)
+
+ def is_authorized(self, owner_id, action=None):
+ if self.is_admin() or owner_id == self.id:
+ return True
+ if action == None:
+ return False
+ project = None #(Fixme)
+ target_object = None # (Fixme, should be passed in)
+ return simplerbac.is_allowed(action, self, project, target_object)
+
+ def get_credentials(self):
+ rc = self.generate_rc()
+ private_key, signed_cert = self.generate_x509_cert()
+
+ tmpdir = tempfile.mkdtemp()
+ zf = os.path.join(tmpdir, "temp.zip")
+ zippy = zipfile.ZipFile(zf, 'w')
+ zippy.writestr(FLAGS.credential_rc_file, rc)
+ zippy.writestr(FLAGS.credential_key_file, private_key)
+ zippy.writestr(FLAGS.credential_cert_file, signed_cert)
+ zippy.writestr(FLAGS.ca_file, crypto.fetch_ca(self.id))
+ zippy.close()
+ with open(zf, 'rb') as f:
+ buffer = f.read()
+
+ shutil.rmtree(tmpdir)
+ return buffer
+
+
+ def generate_rc(self):
+ rc = open(FLAGS.credentials_template).read()
+ rc = rc % { 'access': self.access,
+ 'secret': self.secret,
+ 'ec2': FLAGS.ec2_url,
+ 's3': 'http://%s:%s' % (FLAGS.s3_host, FLAGS.s3_port),
+ 'nova': FLAGS.ca_file,
+ 'cert': FLAGS.credential_cert_file,
+ 'key': FLAGS.credential_key_file,
+ }
+ return rc
+
+ def generate_key_pair(self, name):
+ return self.manager.generate_key_pair(self.id, name)
+
+ def generate_x509_cert(self):
+ return self.manager.generate_x509_cert(self.id)
+
+ def create_key_pair(self, name, public_key, fingerprint):
+ return self.manager.create_key_pair(self.id,
+ name,
+ public_key,
+ fingerprint)
+
+ def get_key_pair(self, name):
+ return self.manager.get_key_pair(self.id, name)
+
+ def delete_key_pair(self, name):
+ return self.manager.delete_key_pair(self.id, name)
+
+ def get_key_pairs(self):
+ return self.manager.get_key_pairs(self.id)
+
+class KeyPair(object):
+ def __init__(self, name, owner, public_key, fingerprint):
+ self.manager = UserManager.instance()
+ self.owner = owner
+ self.name = name
+ self.public_key = public_key
+ self.fingerprint = fingerprint
+
+ def delete(self):
+ return self.manager.delete_key_pair(self.owner, self.name)
+
+class UserManager(object):
+ def __init__(self):
+ if hasattr(self.__class__, '_instance'):
+ raise Exception('Attempted to instantiate singleton')
+
+ @classmethod
+ def instance(cls):
+ if not hasattr(cls, '_instance'):
+ inst = UserManager()
+ cls._instance = inst
+ if FLAGS.fake_users:
+ try:
+ inst.create_user('fake', 'fake', 'fake')
+ except: pass
+ try:
+ inst.create_user('user', 'user', 'user')
+ except: pass
+ try:
+ inst.create_user('admin', 'admin', 'admin', True)
+ except: pass
+ return cls._instance
+
+ def authenticate(self, params, signature, verb='GET', server_string='127.0.0.1:8773', path='/'):
+ # TODO: Check for valid timestamp
+ access_key = params['AWSAccessKeyId']
+ user = self.get_user_from_access_key(access_key)
+ if user == None:
+ return None
+ # hmac can't handle unicode, so encode ensures that secret isn't unicode
+ expected_signature = signer.Signer(user.secret.encode()).generate(params, verb, server_string, path)
+ _log.debug('user.secret: %s', user.secret)
+ _log.debug('expected_signature: %s', expected_signature)
+ _log.debug('signature: %s', signature)
+ if signature == expected_signature:
+ return user
+
+ def has_role(self, user, role, project=None):
+ # Map role to ldap group
+ group = FLAGS.__getitem__("ldap_%s" % role)
+ with LDAPWrapper() as conn:
+ return conn.is_member_of(user, group)
+
+ def add_role(self, user, role, project=None):
+ # TODO: Project-specific roles
+ group = FLAGS.__getitem__("ldap_%s" % role)
+ with LDAPWrapper() as conn:
+ return conn.add_to_group(user, group)
+
+ def get_user(self, uid):
+ with LDAPWrapper() as conn:
+ return conn.find_user(uid)
+
+ def get_user_from_access_key(self, access_key):
+ with LDAPWrapper() as conn:
+ return conn.find_user_by_access_key(access_key)
+
+ def get_users(self):
+ with LDAPWrapper() as conn:
+ return conn.find_users()
+
+ def create_user(self, uid, access=None, secret=None, admin=False):
+ if access == None: access = str(uuid.uuid4())
+ if secret == None: secret = str(uuid.uuid4())
+ with LDAPWrapper() as conn:
+ u = conn.create_user(uid, access, secret, admin)
+ return u
+
+ def delete_user(self, uid):
+ with LDAPWrapper() as conn:
+ conn.delete_user(uid)
+
+ def generate_key_pair(self, uid, key_name):
+ # generating key pair is slow so delay generation
+ # until after check
+ with LDAPWrapper() as conn:
+ if not conn.user_exists(uid):
+ raise UserError("User " + uid + " doesn't exist")
+ if conn.key_pair_exists(uid, key_name):
+ raise InvalidKeyPair("The keypair '" +
+ key_name +
+ "' already exists.",
+ "Duplicate")
+ private_key, public_key, fingerprint = crypto.generate_key_pair()
+ self.create_key_pair(uid, key_name, public_key, fingerprint)
+ return private_key, fingerprint
+
+ def create_key_pair(self, uid, key_name, public_key, fingerprint):
+ with LDAPWrapper() as conn:
+ return conn.create_key_pair(uid, key_name, public_key, fingerprint)
+
+ def get_key_pair(self, uid, key_name):
+ with LDAPWrapper() as conn:
+ return conn.find_key_pair(uid, key_name)
+
+ def get_key_pairs(self, uid):
+ with LDAPWrapper() as conn:
+ return conn.find_key_pairs(uid)
+
+ def delete_key_pair(self, uid, key_name):
+ with LDAPWrapper() as conn:
+ conn.delete_key_pair(uid, key_name)
+
+ def get_signed_zip(self, uid):
+ user = self.get_user(uid)
+ return user.get_credentials()
+
+ def generate_x509_cert(self, uid):
+ (private_key, csr) = crypto.generate_x509_cert(self.__cert_subject(uid))
+ # TODO - This should be async call back to the cloud controller
+ signed_cert = crypto.sign_csr(csr, uid)
+ return (private_key, signed_cert)
+
+ def sign_cert(self, csr, uid):
+ return crypto.sign_csr(csr, uid)
+
+ def __cert_subject(self, uid):
+ return "/C=US/ST=California/L=The_Mission/O=AnsoLabs/OU=Nova/CN=%s-%s" % (uid, str(datetime.datetime.utcnow().isoformat()))
+
+
+class LDAPWrapper(object):
+ def __init__(self):
+ self.user = FLAGS.user_dn
+ self.passwd = FLAGS.ldap_password
+
+ def __enter__(self):
+ self.connect()
+ return self
+
+ def __exit__(self, type, value, traceback):
+ #logging.info('type, value, traceback: %s, %s, %s', type, value, traceback)
+ self.conn.unbind_s()
+ return False
+
+ def connect(self):
+ """ connect to ldap as admin user """
+ if FLAGS.fake_users:
+ self.conn = fakeldap.initialize(FLAGS.ldap_url)
+ else:
+ assert(ldap.__name__ != 'fakeldap')
+ self.conn = ldap.initialize(FLAGS.ldap_url)
+ self.conn.simple_bind_s(self.user, self.passwd)
+
+ def find_object(self, dn, query = None):
+ objects = self.find_objects(dn, query)
+ if len(objects) == 0:
+ return None
+ return objects[0]
+
+ def find_objects(self, dn, query = None):
+ try:
+ res = self.conn.search_s(dn, ldap.SCOPE_SUBTREE, query)
+ except Exception:
+ return []
+ # just return the attributes
+ return [x[1] for x in res]
+
+ def find_users(self):
+ attrs = self.find_objects(FLAGS.ldap_subtree, '(objectclass=novaUser)')
+ return [self.__to_user(attr) for attr in attrs]
+
+ def find_key_pairs(self, uid):
+ dn = 'uid=%s,%s' % (uid, FLAGS.ldap_subtree)
+ attrs = self.find_objects(dn, '(objectclass=novaKeyPair)')
+ return [self.__to_key_pair(uid, attr) for attr in attrs]
+
+ def find_user(self, name):
+ dn = 'uid=%s,%s' % (name, FLAGS.ldap_subtree)
+ attr = self.find_object(dn, '(objectclass=novaUser)')
+ return self.__to_user(attr)
+
+ def user_exists(self, name):
+ return self.find_user(name) != None
+
+ def find_key_pair(self, uid, key_name):
+ dn = 'cn=%s,uid=%s,%s' % (key_name,
+ uid,
+ FLAGS.ldap_subtree)
+ attr = self.find_object(dn, '(objectclass=novaKeyPair)')
+ return self.__to_key_pair(uid, attr)
+
+ def delete_key_pairs(self, uid):
+ keys = self.find_key_pairs(uid)
+ if keys != None:
+ for key in keys:
+ self.delete_key_pair(uid, key.name)
+
+ def key_pair_exists(self, uid, key_name):
+ return self.find_key_pair(uid, key_name) != None
+
+ def create_user(self, name, access_key, secret_key, is_admin):
+ if self.user_exists(name):
+ raise UserError("LDAP user " + name + " already exists")
+ attr = [
+ ('objectclass', ['person',
+ 'organizationalPerson',
+ 'inetOrgPerson',
+ 'novaUser']),
+ ('ou', [FLAGS.user_unit]),
+ ('uid', [name]),
+ ('sn', [name]),
+ ('cn', [name]),
+ ('secretKey', [secret_key]),
+ ('accessKey', [access_key]),
+ ('isAdmin', [str(is_admin).upper()]),
+ ]
+ self.conn.add_s('uid=%s,%s' % (name, FLAGS.ldap_subtree),
+ attr)
+ return self.__to_user(dict(attr))
+
+ def create_project(self, name, project_manager):
+ # PM can be user object or string containing DN
+ pass
+
+ def is_member_of(self, name, group):
+ return True
+
+ def add_to_group(self, name, group):
+ pass
+
+ def remove_from_group(self, name, group):
+ pass
+
+ def create_key_pair(self, uid, key_name, public_key, fingerprint):
+ """create's a public key in the directory underneath the user"""
+ # TODO(vish): possibly refactor this to store keys in their own ou
+ # and put dn reference in the user object
+ attr = [
+ ('objectclass', ['novaKeyPair']),
+ ('cn', [key_name]),
+ ('sshPublicKey', [public_key]),
+ ('keyFingerprint', [fingerprint]),
+ ]
+ self.conn.add_s('cn=%s,uid=%s,%s' % (key_name,
+ uid,
+ FLAGS.ldap_subtree),
+ attr)
+ return self.__to_key_pair(uid, dict(attr))
+
+ def find_user_by_access_key(self, access):
+ query = '(' + 'accessKey' + '=' + access + ')'
+ dn = FLAGS.ldap_subtree
+ return self.__to_user(self.find_object(dn, query))
+
+ def delete_key_pair(self, uid, key_name):
+ if not self.key_pair_exists(uid, key_name):
+ raise UserError("Key Pair " +
+ key_name +
+ " doesn't exist for user " +
+ uid)
+ self.conn.delete_s('cn=%s,uid=%s,%s' % (key_name, uid,
+ FLAGS.ldap_subtree))
+
+ def delete_user(self, name):
+ if not self.user_exists(name):
+ raise UserError("User " +
+ name +
+ " doesn't exist")
+ self.delete_key_pairs(name)
+ self.conn.delete_s('uid=%s,%s' % (name,
+ FLAGS.ldap_subtree))
+
+ def __to_user(self, attr):
+ if attr == None:
+ return None
+ return User(
+ id = attr['uid'][0],
+ name = attr['uid'][0],
+ access = attr['accessKey'][0],
+ secret = attr['secretKey'][0],
+ admin = (attr['isAdmin'][0] == 'TRUE')
+ )
+
+ def __to_key_pair(self, owner, attr):
+ if attr == None:
+ return None
+ return KeyPair(
+ owner = owner,
+ name = attr['cn'][0],
+ public_key = attr['sshPublicKey'][0],
+ fingerprint = attr['keyFingerprint'][0],
+ )