nova/nova/auth/users.py
2010-05-30 15:21:34 -07:00

663 lines
25 KiB
Python

#!/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 string
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
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('user_ldap_subtree', 'ou=Users,dc=example,dc=com', 'OU for Users')
flags.DEFINE_string('project_ldap_subtree', 'ou=Groups,dc=example,dc=com', 'OU for Projects')
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')
class AuthBase(object):
@classmethod
def safe_id(cls, obj):
"""this method will return the id of the object if the object is of this class, otherwise
it will return the original object. This allows methods to accept objects or
ids as paramaters"""
if isinstance(obj, cls):
return obj.id
else:
return obj
class User(AuthBase):
"""id and name are currently the same"""
def __init__(self, id, name, access, secret, admin):
self.id = id
self.name = name
self.access = access
self.secret = secret
self.admin = admin
def is_admin(self):
"""allows user to see objects from all projects"""
return self.admin
def is_project_member(self, project):
return UserManager.instance().is_project_member(self, project)
def is_project_manager(self, project):
return UserManager.instance().is_project_manager(self, project)
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 UserManager.instance().generate_key_pair(self.id, name)
def create_key_pair(self, name, public_key, fingerprint):
return UserManager.instance().create_key_pair(self.id,
name,
public_key,
fingerprint)
def get_key_pair(self, name):
return UserManager.instance().get_key_pair(self.id, name)
def delete_key_pair(self, name):
return UserManager.instance().delete_key_pair(self.id, name)
def get_key_pairs(self):
return UserManager.instance().get_key_pairs(self.id)
def __repr__(self):
return "User('%s', '%s', '%s', '%s', %s)" % (self.id, self.name, self.access, self.secret, self.admin)
class KeyPair(AuthBase):
def __init__(self, id, owner_id, public_key, fingerprint):
self.id = id
self.name = id
self.owner_id = owner_id
self.public_key = public_key
self.fingerprint = fingerprint
def delete(self):
return UserManager.instance().delete_key_pair(self.owner, self.name)
def __repr__(self):
return "KeyPair('%s', '%s', '%s', '%s')" % (self.id, self.owner_id, self.public_key, self.fingerprint)
class Group(AuthBase):
"""id and name are currently the same"""
def __init__(self, id, description = None, member_ids = None):
self.id = id
self.name = id
self.description = description
self.member_ids = member_ids
def has_member(self, user):
return User.safe_id(user) in self.member_ids
def __repr__(self):
return "Group('%s', '%s', %s)" % (self.id, self.description, self.member_ids)
class Project(Group):
def __init__(self, id, project_manager_id, description, member_ids):
self.project_manager_id = project_manager_id
super(Project, self).__init__(id, description, member_ids)
self.keeper = datastore.Keeper(prefix="project-")
@property
def project_manager(self):
return UserManager.instance().get_user(self.project_manager_id)
def has_manager(self, user):
return User.safe_id(user) == self.project_manager_id
def get_credentials(self, user):
rc = user.generate_rc()
private_key, signed_cert = self.generate_x509_cert(user)
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_x509_cert(self, user):
return UserManager.instance().generate_x509_cert(user, self)
def __repr__(self):
return "Project('%s', '%s', '%s', %s)" % (self.id, self.project_manager_id, self.description, self.member_ids)
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, access, signature, params, verb='GET', server_string='127.0.0.1:8773', path='/', verify_signature=True):
# TODO: Check for valid timestamp
(access_key, sep, project_name) = access.partition(':')
user = self.get_user_from_access_key(access_key)
if user == None:
raise exception.NotFound('No user found for access key')
if project_name is '':
project_name = user.name
project = self.get_project(project_name)
if project == None:
raise exception.NotFound('No project called %s could be found' % project_name)
if not user.is_admin() and not project.has_member(user):
raise exception.NotFound('User %s is not a member of project %s' % (user.id, project.id))
if verify_signature:
# 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)
logging.debug('user.secret: %s', user.secret)
logging.debug('expected_signature: %s', expected_signature)
logging.debug('signature: %s', signature)
if signature != expected_signature:
raise exception.NotAuthorized('Signature does not match')
return (user, project)
def create_project(self, name, manager_user, description, member_users=None):
if member_users:
member_users = [User.safe_id(u) for u in member_users]
with LDAPWrapper() as conn:
return conn.create_project(name, User.safe_id(manager_user), description, member_users)
def get_projects(self):
with LDAPWrapper() as conn:
return conn.find_projects()
def get_project(self, project):
with LDAPWrapper() as conn:
return conn.find_project(Project.safe_id(project))
def add_to_project(self, user, project):
with LDAPWrapper() as conn:
return conn.add_to_project(User.safe_id(user), Project.safe_id(project))
def is_project_manager(self, user, project):
if not isinstance(project, Project):
project = self.get_project(project)
return project.has_manager(user)
def is_project_member(self, user, project):
if isinstance(project, Project):
return project.has_member(user)
else:
with LDAPWrapper() as conn:
return conn.is_in_project(User.safe_id(user), project)
def remove_from_project(self, user, project):
with LDAPWrapper() as conn:
return conn.remove_from_project(User.safe_id(user), Project.safe_id(project))
def delete_project(self, project):
with LDAPWrapper() as conn:
return conn.delete_project(Project.safe_id(project))
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, user, access=None, secret=None, admin=False, create_project=True):
if access == None: access = str(uuid.uuid4())
if secret == None: secret = str(uuid.uuid4())
with LDAPWrapper() as conn:
user = User.safe_id(user)
result = conn.create_user(user, access, secret, admin)
if create_project:
conn.create_project(user, user, user)
return result
def delete_user(self, user, delete_project=True):
with LDAPWrapper() as conn:
user = User.safe_id(user)
if delete_project:
try:
conn.delete_project(user)
except exception.NotFound:
pass
conn.delete_user(user)
def generate_key_pair(self, user, key_name):
# generating key pair is slow so delay generation
# until after check
user = User.safe_id(user)
with LDAPWrapper() as conn:
if not conn.user_exists(user):
raise exception.NotFound("User %s doesn't exist" % user)
if conn.key_pair_exists(user, key_name):
raise exception.Duplicate("The keypair %s already exists" % key_name)
private_key, public_key, fingerprint = crypto.generate_key_pair()
self.create_key_pair(User.safe_id(user), key_name, public_key, fingerprint)
return private_key, fingerprint
def create_key_pair(self, user, key_name, public_key, fingerprint):
with LDAPWrapper() as conn:
return conn.create_key_pair(User.safe_id(user), key_name, public_key, fingerprint)
def get_key_pair(self, user, key_name):
with LDAPWrapper() as conn:
return conn.find_key_pair(User.safe_id(user), key_name)
def get_key_pairs(self, user):
with LDAPWrapper() as conn:
return conn.find_key_pairs(User.safe_id(user))
def delete_key_pair(self, user, key_name):
with LDAPWrapper() as conn:
conn.delete_key_pair(User.safe_id(user), key_name)
def generate_x509_cert(self, user, project):
(private_key, csr) = crypto.generate_x509_cert(self.__cert_subject(User.safe_id(user)))
# TODO - This should be async call back to the cloud controller
signed_cert = crypto.sign_csr(csr, Project.safe_id(project))
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.user_ldap_subtree, '(objectclass=novaUser)')
return [self.__to_user(attr) for attr in attrs]
def find_key_pairs(self, uid):
attrs = self.find_objects(self.__uid_to_dn(uid), '(objectclass=novaKeyPair)')
return [self.__to_key_pair(uid, attr) for attr in attrs]
def find_projects(self):
attrs = self.find_objects(FLAGS.project_ldap_subtree, '(objectclass=novaProject)')
return [self.__to_project(attr) for attr in attrs]
def find_groups_with_member(self, tree, dn):
attrs = self.find_objects(tree, '(&(objectclass=groupOfNames)(member=%s))' % dn )
return [self.__to_group(attr) for attr in attrs]
def find_user(self, uid):
attr = self.find_object(self.__uid_to_dn(uid), '(objectclass=novaUser)')
return self.__to_user(attr)
def find_key_pair(self, uid, key_name):
dn = 'cn=%s,%s' % (key_name,
self.__uid_to_dn(uid))
attr = self.find_object(dn, '(objectclass=novaKeyPair)')
return self.__to_key_pair(uid, attr)
def find_group(self, dn):
"""uses dn directly instead of custructing it from name"""
attr = self.find_object(dn, '(objectclass=groupOfNames)')
return self.__to_group(attr)
def find_project(self, name):
dn = 'cn=%s,%s' % (name,
FLAGS.project_ldap_subtree)
attr = self.find_object(dn, '(objectclass=novaProject)')
return self.__to_project(attr)
def user_exists(self, name):
return self.find_user(name) != None
def key_pair_exists(self, uid, key_name):
return self.find_key_pair(uid, key_name) != None
def project_exists(self, name):
return self.find_project(name) != None
def group_exists(self, dn):
return self.find_group(dn) != None
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 create_user(self, name, access_key, secret_key, is_admin):
if self.user_exists(name):
raise exception.Duplicate("LDAP user %s already exists" % name)
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(self.__uid_to_dn(name), attr)
return self.__to_user(dict(attr))
def create_project(self, name, manager_uid, description, member_uids = None):
if self.project_exists(name):
raise exception.Duplicate("Project can't be created because project %s already exists" % name)
if not self.user_exists(manager_uid):
raise exception.NotFound("Project can't be created because manager %s doesn't exist" % manager_uid)
manager_dn = self.__uid_to_dn(manager_uid)
members = []
if member_uids != None:
for member_uid in member_uids:
if not self.user_exists(member_uid):
raise exception.NotFound("Project can't be created because user %s doesn't exist" % member_uid)
members.append(self.__uid_to_dn(member_uid))
# always add the manager as a member because members is required
if not manager_dn in members:
members.append(manager_dn)
attr = [
('objectclass', ['novaProject']),
('cn', [name]),
('description', [description]),
('projectManager', [manager_dn]),
('member', members)
]
self.conn.add_s('cn=%s,%s' % (name, FLAGS.project_ldap_subtree), attr)
return self.__to_project(dict(attr))
def add_to_project(self, uid, project_id):
dn = 'cn=%s,%s' % (project_id, FLAGS.project_ldap_subtree)
return self.add_to_group(uid, dn)
def remove_from_project(self, uid, project_id):
dn = 'cn=%s,%s' % (project_id, FLAGS.project_ldap_subtree)
return self.remove_from_group(uid, dn)
def is_in_project(self, uid, project_id):
dn = 'cn=%s,%s' % (project_id, FLAGS.project_ldap_subtree)
return self.is_in_group(uid, dn)
def __create_group(self, group_dn, name, uid, description, member_uids = None):
if self.group_exists(name):
raise exception.Duplicate("Group can't be created because group %s already exists" % name)
members = []
if member_uids != None:
for member_uid in member_uids:
if not self.user_exists(member_uid):
raise exception.NotFound("Group can't be created because user %s doesn't exist" % member_uid)
members.append(self.__uid_to_dn(member_uid))
dn = self.__uid_to_dn(uid)
if not dn in members:
members.append(dn)
attr = [
('objectclass', ['groupOfNames']),
('cn', [name]),
('description', [description]),
('member', members)
]
self.conn.add_s(group_dn, attr)
return self.__to_group(dict(attr))
def is_in_group(self, uid, group_dn):
if not self.user_exists(uid):
raise exception.NotFound("User %s can't be searched in group becuase the user doesn't exist" % (uid,))
if not self.group_exists(group_dn):
return False
res = self.find_object(group_dn,
'(member=%s)' % self.__uid_to_dn(uid))
return res != None
def add_to_group(self, uid, group_dn):
if not self.user_exists(uid):
raise exception.NotFound("User %s can't be added to the group becuase the user doesn't exist" % (uid,))
if not self.group_exists(group_dn):
raise exception.NotFound("The group at dn %s doesn't exist" % (group_dn,))
if self.is_in_group(uid, group_dn):
raise exception.Duplicate("User %s is already a member of the group %s" % (uid, group_dn))
attr = [
(ldap.MOD_ADD, 'member', self.__uid_to_dn(uid))
]
self.conn.modify_s(group_dn, attr)
def remove_from_group(self, uid, group_dn):
if not self.group_exists(group_dn):
raise exception.NotFound("The group at dn %s doesn't exist" % (group_dn,))
if not self.user_exists(uid):
raise exception.NotFound("User %s can't be removed from the group because the user doesn't exist" % (uid,))
if not self.is_in_group(uid, group_dn):
raise exception.NotFound("User %s is not a member of the group" % (uid,))
attr = [
(ldap.MOD_DELETE, 'member', self.__uid_to_dn(uid))
]
try:
self.conn.modify_s(group_dn, attr)
except ldap.OBJECT_CLASS_VIOLATION:
logging.debug("Attempted to remove the last member of a group. Deleting the group instead.")
self.delete_group(group_dn)
def remove_from_all(self, uid):
# FIXME(vish): what if deleted user is a project manager?
if not self.user_exists(uid):
raise exception.NotFound("User %s can't be removed from all because the user doesn't exist" % (uid,))
dn = self.__uid_to_dn(uid)
attr = [
(ldap.MOD_DELETE, 'member', dn)
]
projects = self.find_groups_with_member(FLAGS.project_ldap_subtree, dn)
for project in projects:
self.conn.modify_s('cn=%s,%s' % (project.id, FLAGS.project_ldap_subtree), attr)
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,%s' % (key_name,
self.__uid_to_dn(uid)),
attr)
return self.__to_key_pair(uid, dict(attr))
def find_user_by_access_key(self, access):
query = '(accessKey=%s)' % access
dn = FLAGS.user_ldap_subtree
return self.__to_user(self.find_object(dn, query))
def delete_user(self, uid):
if not self.user_exists(uid):
raise exception.NotFound("User %s doesn't exist" % uid)
self.delete_key_pairs(uid)
self.remove_from_all(uid)
self.conn.delete_s('uid=%s,%s' % (uid,
FLAGS.user_ldap_subtree))
def delete_key_pair(self, uid, key_name):
if not self.key_pair_exists(uid, key_name):
raise exception.NotFound("Key Pair %s doesn't exist for user %s" %
(key_name, uid))
self.conn.delete_s('cn=%s,uid=%s,%s' % (key_name, uid,
FLAGS.user_ldap_subtree))
def delete_group(self, group_dn):
if not self.group_exists(group_dn):
raise exception.NotFound("Group at dn %s doesn't exist" % group_dn)
self.conn.delete_s(group_dn)
def delete_project(self, name):
project_dn = 'cn=%s,%s' % (name, FLAGS.project_ldap_subtree)
self.delete_group(project_dn)
def __to_user(self, attr):
if attr == None:
return None
return User(
id = attr['uid'][0],
name = attr['cn'][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(
id = attr['cn'][0],
owner_id = owner,
public_key = attr['sshPublicKey'][0],
fingerprint = attr['keyFingerprint'][0],
)
def __to_group(self, attr):
if attr == None:
return None
member_dns = attr.get('member', [])
return Group(
id = attr['cn'][0],
description = attr.get('description', [None])[0],
member_ids = [self.__dn_to_uid(x) for x in member_dns]
)
def __to_project(self, attr):
if attr == None:
return None
member_dns = attr.get('member', [])
return Project(
id = attr['cn'][0],
project_manager_id = self.__dn_to_uid(attr['projectManager'][0]),
description = attr.get('description', [None])[0],
member_ids = [self.__dn_to_uid(x) for x in member_dns]
)
def __dn_to_uid(self, dn):
return dn.split(',')[0].split('=')[1]
def __uid_to_dn(self, dn):
return 'uid=%s,%s' % (dn, FLAGS.user_ldap_subtree)