1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
3
# Copyright 2010 United States Government as represented by the
4
# Administrator of the National Aeronautics and Space Administration.
7
# Licensed under the Apache License, Version 2.0 (the "License"); you may
8
# not use this file except in compliance with the License. You may obtain
9
# a copy of the License at
11
# http://www.apache.org/licenses/LICENSE-2.0
13
# Unless required by applicable law or agreed to in writing, software
14
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16
# License for the specific language governing permissions and limitations
20
Auth driver for ldap. Includes FakeLdapDriver.
22
It should be easy to create a replacement for this driver supporting
23
other backends by creating another class that exposes the same
30
from nova import exception
31
from nova import flags
32
from nova.openstack.common import cfg
33
from nova.openstack.common import log as logging
37
cfg.IntOpt('ldap_schema_version',
39
help='Current version of the LDAP schema'),
40
cfg.StrOpt('ldap_url',
41
default='ldap://localhost',
42
help='Point this at your ldap server'),
43
cfg.StrOpt('ldap_password',
45
help='LDAP password'),
46
cfg.StrOpt('ldap_user_dn',
47
default='cn=Manager,dc=example,dc=com',
48
help='DN of admin user'),
49
cfg.StrOpt('ldap_user_id_attribute',
51
help='Attribute to use as id'),
52
cfg.StrOpt('ldap_user_name_attribute',
54
help='Attribute to use as name'),
55
cfg.StrOpt('ldap_user_unit',
57
help='OID for Users'),
58
cfg.StrOpt('ldap_user_subtree',
59
default='ou=Users,dc=example,dc=com',
61
cfg.BoolOpt('ldap_user_modify_only',
63
help='Modify user attributes instead of creating/deleting'),
64
cfg.StrOpt('ldap_project_subtree',
65
default='ou=Groups,dc=example,dc=com',
66
help='OU for Projects'),
67
cfg.StrOpt('role_project_subtree',
68
default='ou=Groups,dc=example,dc=com',
71
# NOTE(vish): mapping with these flags is necessary because we're going
72
# to tie in to an existing ldap schema
73
cfg.StrOpt('ldap_cloudadmin',
74
default='cn=cloudadmins,ou=Groups,dc=example,dc=com',
75
help='cn for Cloud Admins'),
76
cfg.StrOpt('ldap_itsec',
77
default='cn=itsec,ou=Groups,dc=example,dc=com',
79
cfg.StrOpt('ldap_sysadmin',
80
default='cn=sysadmins,ou=Groups,dc=example,dc=com',
81
help='cn for Sysadmins'),
82
cfg.StrOpt('ldap_netadmin',
83
default='cn=netadmins,ou=Groups,dc=example,dc=com',
84
help='cn for NetAdmins'),
85
cfg.StrOpt('ldap_developer',
86
default='cn=developers,ou=Groups,dc=example,dc=com',
87
help='cn for Developers'),
91
FLAGS.register_opts(ldap_opts)
93
LOG = logging.getLogger(__name__)
96
if FLAGS.memcached_servers:
99
from nova.common import memorycache as memcache
102
# TODO(vish): make an abstract base class with the same public methods
103
# to define a set interface for AuthDrivers. I'm delaying
104
# creating this now because I'm expecting an auth refactor
105
# in which we may want to change the interface a bit more.
109
"""Clean attr for insertion into ldap"""
112
if isinstance(attr, unicode):
118
"""Decorator to sanitize all args"""
120
def _wrapped(self, *args, **kwargs):
121
args = [_clean(x) for x in args]
122
kwargs = dict((k, _clean(v)) for (k, v) in kwargs)
123
return fn(self, *args, **kwargs)
124
_wrapped.func_name = fn.func_name
128
class LDAPWrapper(object):
129
def __init__(self, ldap, url, user, password):
133
self.password = password
136
def __wrap_reconnect(f):
137
def inner(self, *args, **kwargs):
138
if self.conn is None:
140
return f(self.conn)(*args, **kwargs)
143
return f(self.conn)(*args, **kwargs)
144
except self.ldap.SERVER_DOWN:
146
return f(self.conn)(*args, **kwargs)
151
self.conn = self.ldap.initialize(self.url)
152
self.conn.simple_bind_s(self.user, self.password)
153
except self.ldap.SERVER_DOWN:
157
search_s = __wrap_reconnect(lambda conn: conn.search_s)
158
add_s = __wrap_reconnect(lambda conn: conn.add_s)
159
delete_s = __wrap_reconnect(lambda conn: conn.delete_s)
160
modify_s = __wrap_reconnect(lambda conn: conn.modify_s)
163
class LdapDriver(object):
166
Defines enter and exit and therefore supports the with/as syntax.
169
project_pattern = '(owner=*)'
170
isadmin_attribute = 'isNovaAdmin'
171
project_attribute = 'owner'
172
project_objectclass = 'groupOfNames'
177
"""Imports the LDAP module"""
178
self.ldap = __import__('ldap')
179
if FLAGS.ldap_schema_version == 1:
180
LdapDriver.project_pattern = '(objectclass=novaProject)'
181
LdapDriver.isadmin_attribute = 'isAdmin'
182
LdapDriver.project_attribute = 'projectManager'
183
LdapDriver.project_objectclass = 'novaProject'
185
if LdapDriver.conn is None:
186
LdapDriver.conn = LDAPWrapper(self.ldap, FLAGS.ldap_url,
189
if LdapDriver.mc is None:
190
LdapDriver.mc = memcache.Client(FLAGS.memcached_servers, debug=0)
193
# TODO(yorik-sar): Should be per-request cache, not per-driver-request
197
def __exit__(self, exc_type, exc_value, traceback):
201
def __local_cache(key_fmt): # pylint: disable=E0213
202
"""Wrap function to cache its result in self.__cache.
203
Works only with functions with one fixed argument.
207
def inner(self, arg, **kwargs):
208
cache_key = key_fmt % (arg,)
210
res = self.__cache[cache_key]
211
LOG.debug('Local cache hit for %s by key %s' %
212
(fn.__name__, cache_key))
215
res = fn(self, arg, **kwargs)
216
self.__cache[cache_key] = res
222
@__local_cache('uid_user-%s')
223
def get_user(self, uid):
224
"""Retrieve user by id"""
225
attr = self.__get_ldap_user(uid)
227
raise exception.LDAPUserNotFound(user_id=uid)
228
return self.__to_user(attr)
231
def get_user_from_access_key(self, access):
232
"""Retrieve user by access key"""
233
cache_key = 'uak_dn_%s' % (access,)
234
user_dn = self.mc.get(cache_key)
236
user = self.__to_user(
237
self.__find_object(user_dn, scope=self.ldap.SCOPE_BASE))
239
if user['access'] == access:
242
self.mc.set(cache_key, None)
243
query = '(accessKey=%s)' % access
244
dn = FLAGS.ldap_user_subtree
245
user_obj = self.__find_object(dn, query)
246
user = self.__to_user(user_obj)
248
self.mc.set(cache_key, user_obj['dn'][0])
252
@__local_cache('pid_project-%s')
253
def get_project(self, pid):
254
"""Retrieve project by id"""
255
dn = self.__project_to_dn(pid, search=False)
256
attr = self.__find_object(dn, LdapDriver.project_pattern,
257
scope=self.ldap.SCOPE_BASE)
258
return self.__to_project(attr)
262
"""Retrieve list of users"""
263
attrs = self.__find_objects(FLAGS.ldap_user_subtree,
264
'(objectclass=novaUser)')
267
user = self.__to_user(attr)
273
def get_projects(self, uid=None):
274
"""Retrieve list of projects"""
275
pattern = LdapDriver.project_pattern
277
pattern = "(&%s(member=%s))" % (pattern, self.__uid_to_dn(uid))
278
attrs = self.__find_objects(FLAGS.ldap_project_subtree,
280
return [self.__to_project(attr) for attr in attrs]
283
def create_user(self, name, access_key, secret_key, is_admin):
285
if self.__user_exists(name):
286
raise exception.LDAPUserExists(user=name)
287
if FLAGS.ldap_user_modify_only:
288
if self.__ldap_user_exists(name):
289
# Retrieve user by name
290
user = self.__get_ldap_user(name)
291
# Entry could be malformed, test for missing attrs.
292
# Malformed entries are useless, replace attributes found.
294
if 'secretKey' in user.keys():
295
attr.append((self.ldap.MOD_REPLACE, 'secretKey',
298
attr.append((self.ldap.MOD_ADD, 'secretKey',
300
if 'accessKey' in user.keys():
301
attr.append((self.ldap.MOD_REPLACE, 'accessKey',
304
attr.append((self.ldap.MOD_ADD, 'accessKey',
306
if LdapDriver.isadmin_attribute in user.keys():
307
attr.append((self.ldap.MOD_REPLACE,
308
LdapDriver.isadmin_attribute,
309
[str(is_admin).upper()]))
311
attr.append((self.ldap.MOD_ADD,
312
LdapDriver.isadmin_attribute,
313
[str(is_admin).upper()]))
314
self.conn.modify_s(self.__uid_to_dn(name), attr)
315
return self.get_user(name)
317
raise exception.LDAPUserNotFound(user_id=name)
320
('objectclass', ['person',
321
'organizationalPerson',
324
('ou', [FLAGS.ldap_user_unit]),
325
(FLAGS.ldap_user_id_attribute, [name]),
327
(FLAGS.ldap_user_name_attribute, [name]),
328
('secretKey', [secret_key]),
329
('accessKey', [access_key]),
330
(LdapDriver.isadmin_attribute, [str(is_admin).upper()]),
332
self.conn.add_s(self.__uid_to_dn(name), attr)
333
return self.__to_user(dict(attr))
336
def create_project(self, name, manager_uid,
337
description=None, member_uids=None):
338
"""Create a project"""
339
if self.__project_exists(name):
340
raise exception.ProjectExists(project=name)
341
if not self.__user_exists(manager_uid):
342
raise exception.LDAPUserNotFound(user_id=manager_uid)
343
manager_dn = self.__uid_to_dn(manager_uid)
344
# description is a required attribute
345
if description is None:
348
if member_uids is not None:
349
for member_uid in member_uids:
350
if not self.__user_exists(member_uid):
351
raise exception.LDAPUserNotFound(user_id=member_uid)
352
members.append(self.__uid_to_dn(member_uid))
353
# always add the manager as a member because members is required
354
if not manager_dn in members:
355
members.append(manager_dn)
357
('objectclass', [LdapDriver.project_objectclass]),
359
('description', [description]),
360
(LdapDriver.project_attribute, [manager_dn]),
362
dn = self.__project_to_dn(name, search=False)
363
self.conn.add_s(dn, attr)
364
return self.__to_project(dict(attr))
367
def modify_project(self, project_id, manager_uid=None, description=None):
368
"""Modify an existing project"""
369
if not manager_uid and not description:
373
if not self.__user_exists(manager_uid):
374
raise exception.LDAPUserNotFound(user_id=manager_uid)
375
manager_dn = self.__uid_to_dn(manager_uid)
376
attr.append((self.ldap.MOD_REPLACE, LdapDriver.project_attribute,
379
attr.append((self.ldap.MOD_REPLACE, 'description', description))
380
dn = self.__project_to_dn(project_id)
381
self.conn.modify_s(dn, attr)
382
if not self.is_in_project(manager_uid, project_id):
383
self.add_to_project(manager_uid, project_id)
386
def add_to_project(self, uid, project_id):
387
"""Add user to project"""
388
dn = self.__project_to_dn(project_id)
389
return self.__add_to_group(uid, dn)
392
def remove_from_project(self, uid, project_id):
393
"""Remove user from project"""
394
dn = self.__project_to_dn(project_id)
395
return self.__remove_from_group(uid, dn)
398
def is_in_project(self, uid, project_id):
399
"""Check if user is in project"""
400
dn = self.__project_to_dn(project_id)
401
return self.__is_in_group(uid, dn)
404
def has_role(self, uid, role, project_id=None):
405
"""Check if user has role
407
If project is specified, it checks for local role, otherwise it
408
checks for global role
410
role_dn = self.__role_to_dn(role, project_id)
411
return self.__is_in_group(uid, role_dn)
414
def add_role(self, uid, role, project_id=None):
415
"""Add role for user (or user and project)"""
416
role_dn = self.__role_to_dn(role, project_id)
417
if not self.__group_exists(role_dn):
418
# create the role if it doesn't exist
419
description = '%s role for %s' % (role, project_id)
420
self.__create_group(role_dn, role, uid, description)
422
return self.__add_to_group(uid, role_dn)
425
def remove_role(self, uid, role, project_id=None):
426
"""Remove role for user (or user and project)"""
427
role_dn = self.__role_to_dn(role, project_id)
428
return self.__remove_from_group(uid, role_dn)
431
def get_user_roles(self, uid, project_id=None):
432
"""Retrieve list of roles for user (or user and project)"""
433
if project_id is None:
434
# NOTE(vish): This is unneccesarily slow, but since we can't
435
# guarantee that the global roles are located
436
# together in the ldap tree, we're doing this version.
438
for role in FLAGS.allowed_roles:
439
role_dn = self.__role_to_dn(role)
440
if self.__is_in_group(uid, role_dn):
444
project_dn = self.__project_to_dn(project_id)
445
query = ('(&(&(objectclass=groupOfNames)(!%s))(member=%s))' %
446
(LdapDriver.project_pattern, self.__uid_to_dn(uid)))
447
roles = self.__find_objects(project_dn, query)
448
return [role['cn'][0] for role in roles]
451
def delete_user(self, uid):
453
if not self.__user_exists(uid):
454
raise exception.LDAPUserNotFound(user_id=uid)
455
self.__remove_from_all(uid)
456
if FLAGS.ldap_user_modify_only:
459
# Retrieve user by name
460
user = self.__get_ldap_user(uid)
461
if 'secretKey' in user.keys():
462
attr.append((self.ldap.MOD_DELETE, 'secretKey',
464
if 'accessKey' in user.keys():
465
attr.append((self.ldap.MOD_DELETE, 'accessKey',
467
if LdapDriver.isadmin_attribute in user.keys():
468
attr.append((self.ldap.MOD_DELETE,
469
LdapDriver.isadmin_attribute,
470
user[LdapDriver.isadmin_attribute]))
471
self.conn.modify_s(self.__uid_to_dn(uid), attr)
474
self.conn.delete_s(self.__uid_to_dn(uid))
477
def delete_project(self, project_id):
478
"""Delete a project"""
479
project_dn = self.__project_to_dn(project_id)
480
self.__delete_roles(project_dn)
481
self.__delete_group(project_dn)
484
def modify_user(self, uid, access_key=None, secret_key=None, admin=None):
485
"""Modify an existing user"""
486
if not access_key and not secret_key and admin is None:
490
attr.append((self.ldap.MOD_REPLACE, 'accessKey', access_key))
492
attr.append((self.ldap.MOD_REPLACE, 'secretKey', secret_key))
493
if admin is not None:
494
attr.append((self.ldap.MOD_REPLACE, LdapDriver.isadmin_attribute,
496
self.conn.modify_s(self.__uid_to_dn(uid), attr)
498
def __user_exists(self, uid):
499
"""Check if user exists"""
501
return self.get_user(uid) is not None
502
except exception.LDAPUserNotFound:
505
def __ldap_user_exists(self, uid):
506
"""Check if the user exists in ldap"""
507
return self.__get_ldap_user(uid) is not None
509
def __project_exists(self, project_id):
510
"""Check if project exists"""
511
return self.get_project(project_id) is not None
513
@__local_cache('uid_attrs-%s')
514
def __get_ldap_user(self, uid):
515
"""Retrieve LDAP user entry by id"""
516
dn = FLAGS.ldap_user_subtree
517
query = ('(&(%s=%s)(objectclass=novaUser))' %
518
(FLAGS.ldap_user_id_attribute, uid))
519
return self.__find_object(dn, query)
521
def __find_object(self, dn, query=None, scope=None):
522
"""Find an object by dn and query"""
523
objects = self.__find_objects(dn, query, scope)
524
if len(objects) == 0:
528
def __find_dns(self, dn, query=None, scope=None):
529
"""Find dns by query"""
531
# One of the flags is 0!
532
scope = self.ldap.SCOPE_SUBTREE
534
res = self.conn.search_s(dn, scope, query)
535
except self.ldap.NO_SUCH_OBJECT:
537
# Just return the DNs
538
return [dn for dn, _attributes in res]
540
def __find_objects(self, dn, query=None, scope=None):
541
"""Find objects by query"""
543
# One of the flags is 0!
544
scope = self.ldap.SCOPE_SUBTREE
546
query = "(objectClass=*)"
548
res = self.conn.search_s(dn, scope, query)
549
except self.ldap.NO_SUCH_OBJECT:
551
# Just return the attributes
552
# FIXME(yorik-sar): Whole driver should be refactored to
555
for dn, attrs in res:
560
def __find_role_dns(self, tree):
561
"""Find dns of role objects in given tree"""
562
query = ('(&(objectclass=groupOfNames)(!%s))' %
563
LdapDriver.project_pattern)
564
return self.__find_dns(tree, query)
566
def __find_group_dns_with_member(self, tree, uid):
567
"""Find dns of group objects in a given tree that contain member"""
568
query = ('(&(objectclass=groupOfNames)(member=%s))' %
569
self.__uid_to_dn(uid))
570
dns = self.__find_dns(tree, query)
573
def __group_exists(self, dn):
574
"""Check if group exists"""
575
query = '(objectclass=groupOfNames)'
576
return self.__find_object(dn, query) is not None
578
def __role_to_dn(self, role, project_id=None):
579
"""Convert role to corresponding dn"""
580
if project_id is None:
581
return FLAGS["ldap_%s" % role]
583
project_dn = self.__project_to_dn(project_id)
584
return 'cn=%s,%s' % (role, project_dn)
586
def __create_group(self, group_dn, name, uid,
587
description, member_uids=None):
589
if self.__group_exists(group_dn):
590
raise exception.LDAPGroupExists(group=name)
592
if member_uids is not None:
593
for member_uid in member_uids:
594
if not self.__user_exists(member_uid):
595
raise exception.LDAPUserNotFound(user_id=member_uid)
596
members.append(self.__uid_to_dn(member_uid))
597
dn = self.__uid_to_dn(uid)
598
if not dn in members:
601
('objectclass', ['groupOfNames']),
603
('description', [description]),
605
self.conn.add_s(group_dn, attr)
607
def __is_in_group(self, uid, group_dn):
608
"""Check if user is in group"""
609
if not self.__user_exists(uid):
610
raise exception.LDAPUserNotFound(user_id=uid)
611
if not self.__group_exists(group_dn):
613
res = self.__find_object(group_dn,
614
'(member=%s)' % self.__uid_to_dn(uid),
615
self.ldap.SCOPE_BASE)
616
return res is not None
618
def __add_to_group(self, uid, group_dn):
619
"""Add user to group"""
620
if not self.__user_exists(uid):
621
raise exception.LDAPUserNotFound(user_id=uid)
622
if not self.__group_exists(group_dn):
623
raise exception.LDAPGroupNotFound(group_id=group_dn)
624
if self.__is_in_group(uid, group_dn):
625
raise exception.LDAPMembershipExists(uid=uid, group_dn=group_dn)
626
attr = [(self.ldap.MOD_ADD, 'member', self.__uid_to_dn(uid))]
627
self.conn.modify_s(group_dn, attr)
629
def __remove_from_group(self, uid, group_dn):
630
"""Remove user from group"""
631
if not self.__group_exists(group_dn):
632
raise exception.LDAPGroupNotFound(group_id=group_dn)
633
if not self.__user_exists(uid):
634
raise exception.LDAPUserNotFound(user_id=uid)
635
if not self.__is_in_group(uid, group_dn):
636
raise exception.LDAPGroupMembershipNotFound(user_id=uid,
638
# NOTE(vish): remove user from group and any sub_groups
639
sub_dns = self.__find_group_dns_with_member(group_dn, uid)
640
for sub_dn in sub_dns:
641
self.__safe_remove_from_group(uid, sub_dn)
643
def __safe_remove_from_group(self, uid, group_dn):
644
"""Remove user from group, deleting group if user is last member"""
645
# FIXME(vish): what if deleted user is a project manager?
646
attr = [(self.ldap.MOD_DELETE, 'member', self.__uid_to_dn(uid))]
648
self.conn.modify_s(group_dn, attr)
649
except self.ldap.OBJECT_CLASS_VIOLATION:
650
LOG.debug(_("Attempted to remove the last member of a group. "
651
"Deleting the group at %s instead."), group_dn)
652
self.__delete_group(group_dn)
654
def __remove_from_all(self, uid):
655
"""Remove user from all roles and projects"""
656
if not self.__user_exists(uid):
657
raise exception.LDAPUserNotFound(user_id=uid)
658
role_dns = self.__find_group_dns_with_member(
659
FLAGS.role_project_subtree, uid)
660
for role_dn in role_dns:
661
self.__safe_remove_from_group(uid, role_dn)
662
project_dns = self.__find_group_dns_with_member(
663
FLAGS.ldap_project_subtree, uid)
664
for project_dn in project_dns:
665
self.__safe_remove_from_group(uid, project_dn)
667
def __delete_group(self, group_dn):
669
if not self.__group_exists(group_dn):
670
raise exception.LDAPGroupNotFound(group_id=group_dn)
671
self.conn.delete_s(group_dn)
673
def __delete_roles(self, project_dn):
674
"""Delete all roles for project"""
675
for role_dn in self.__find_role_dns(project_dn):
676
self.__delete_group(role_dn)
678
def __to_project(self, attr):
679
"""Convert ldap attributes to Project object"""
682
member_dns = attr.get('member', [])
685
'name': attr['cn'][0],
686
'project_manager_id':
687
self.__dn_to_uid(attr[LdapDriver.project_attribute][0]),
688
'description': attr.get('description', [None])[0],
689
'member_ids': [self.__dn_to_uid(x) for x in member_dns]}
691
@__local_cache('uid_dn-%s')
692
def __uid_to_dn(self, uid, search=True):
693
"""Convert uid to dn"""
694
# By default return a generated DN
695
userdn = (FLAGS.ldap_user_id_attribute + '=%s,%s'
696
% (uid, FLAGS.ldap_user_subtree))
698
query = ('%s=%s' % (FLAGS.ldap_user_id_attribute, uid))
699
user = self.__find_dns(FLAGS.ldap_user_subtree, query)
704
@__local_cache('pid_dn-%s')
705
def __project_to_dn(self, pid, search=True):
706
"""Convert pid to dn"""
707
# By default return a generated DN
708
projectdn = ('cn=%s,%s' % (pid, FLAGS.ldap_project_subtree))
710
query = ('(&(cn=%s)%s)' % (pid, LdapDriver.project_pattern))
711
project = self.__find_dns(FLAGS.ldap_project_subtree, query)
713
projectdn = project[0]
718
"""Convert ldap attributes to User object"""
721
if ('accessKey' in attr.keys() and 'secretKey' in attr.keys() and
722
LdapDriver.isadmin_attribute in attr.keys()):
724
'id': attr[FLAGS.ldap_user_id_attribute][0],
725
'name': attr[FLAGS.ldap_user_name_attribute][0],
726
'access': attr['accessKey'][0],
727
'secret': attr['secretKey'][0],
728
'admin': (attr[LdapDriver.isadmin_attribute][0] == 'TRUE')}
732
@__local_cache('dn_uid-%s')
733
def __dn_to_uid(self, dn):
734
"""Convert user dn to uid"""
735
query = '(objectclass=novaUser)'
736
user = self.__find_object(dn, query, scope=self.ldap.SCOPE_BASE)
737
return user[FLAGS.ldap_user_id_attribute][0]
740
class FakeLdapDriver(LdapDriver):
741
"""Fake Ldap Auth driver"""
744
import nova.auth.fakeldap
745
sys.modules['ldap'] = nova.auth.fakeldap
746
super(FakeLdapDriver, self).__init__()