From bf6e6e718cdc7488e2da87b21e258ccc065fe499 Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Thu, 27 May 2010 23:05:26 -0700 Subject: initial commit --- nova/auth/__init__.py | 25 +++ nova/auth/access.py | 69 +++++++ nova/auth/fakeldap.py | 81 +++++++++ nova/auth/novarc.template | 26 +++ nova/auth/rbac.ldif | 60 ++++++ nova/auth/signer.py | 127 +++++++++++++ nova/auth/slap.sh | 226 +++++++++++++++++++++++ nova/auth/users.py | 454 ++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 1068 insertions(+) create mode 100644 nova/auth/__init__.py create mode 100644 nova/auth/access.py create mode 100644 nova/auth/fakeldap.py create mode 100644 nova/auth/novarc.template create mode 100644 nova/auth/rbac.ldif create mode 100644 nova/auth/signer.py create mode 100755 nova/auth/slap.sh create mode 100755 nova/auth/users.py (limited to 'nova/auth') 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 +.. moduleauthor:: Vishvananda Ishaya +.. moduleauthor:: Joshua McKenty +""" \ 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 < +# +# 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 < +# +# + +# 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 </etc/ldap/ldap.conf </etc/ldap/base.ldif <