diff options
| author | Jesse Andrews <anotherjesse@gmail.com> | 2010-05-27 23:05:26 -0700 |
|---|---|---|
| committer | Jesse Andrews <anotherjesse@gmail.com> | 2010-05-27 23:05:26 -0700 |
| commit | bf6e6e718cdc7488e2da87b21e258ccc065fe499 (patch) | |
| tree | 51cf4f72047eb6b16079c7fe21e9822895541801 /nova/auth | |
| download | nova-bf6e6e718cdc7488e2da87b21e258ccc065fe499.tar.gz nova-bf6e6e718cdc7488e2da87b21e258ccc065fe499.tar.xz nova-bf6e6e718cdc7488e2da87b21e258ccc065fe499.zip | |
initial commit
Diffstat (limited to 'nova/auth')
| -rw-r--r-- | nova/auth/__init__.py | 25 | ||||
| -rw-r--r-- | nova/auth/access.py | 69 | ||||
| -rw-r--r-- | nova/auth/fakeldap.py | 81 | ||||
| -rw-r--r-- | nova/auth/novarc.template | 26 | ||||
| -rw-r--r-- | nova/auth/rbac.ldif | 60 | ||||
| -rw-r--r-- | nova/auth/signer.py | 127 | ||||
| -rwxr-xr-x | nova/auth/slap.sh | 226 | ||||
| -rwxr-xr-x | nova/auth/users.py | 454 |
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], + ) |
