diff options
author | Jenkins <jenkins@review.openstack.org> | 2013-08-16 17:34:56 +0000 |
---|---|---|
committer | Gerrit Code Review <review@openstack.org> | 2013-08-16 17:34:56 +0000 |
commit | 81534a182a4986d838591395aee8590ef61c599d (patch) | |
tree | 94570b8b09e12a079085f85e4381a60175a93235 /keystone | |
parent | d695d4a3cd747ba47e2dfa1ca7e688175cbd06be (diff) | |
parent | 1ed2046eaa91fa36926d66a5fe1e88ccd65373bb (diff) | |
download | keystone-81534a182a4986d838591395aee8590ef61c599d.tar.gz keystone-81534a182a4986d838591395aee8590ef61c599d.tar.xz keystone-81534a182a4986d838591395aee8590ef61c599d.zip |
Merge "Implement domain specific Identity backends"
Diffstat (limited to 'keystone')
-rw-r--r-- | keystone/auth/plugins/password.py | 4 | ||||
-rw-r--r-- | keystone/catalog/backends/templated.py | 3 | ||||
-rw-r--r-- | keystone/common/config.py | 530 | ||||
-rw-r--r-- | keystone/common/controller.py | 51 | ||||
-rw-r--r-- | keystone/common/ldap/fakeldap.py | 16 | ||||
-rw-r--r-- | keystone/common/utils.py | 1 | ||||
-rw-r--r-- | keystone/config.py | 9 | ||||
-rw-r--r-- | keystone/identity/backends/kvs.py | 3 | ||||
-rw-r--r-- | keystone/identity/backends/ldap.py | 52 | ||||
-rw-r--r-- | keystone/identity/backends/pam.py | 3 | ||||
-rw-r--r-- | keystone/identity/backends/sql.py | 3 | ||||
-rw-r--r-- | keystone/identity/controllers.py | 52 | ||||
-rw-r--r-- | keystone/identity/core.py | 335 | ||||
-rw-r--r-- | keystone/tests/backend_multi_ldap_sql.conf | 35 | ||||
-rw-r--r-- | keystone/tests/core.py | 8 | ||||
-rw-r--r-- | keystone/tests/keystone.Default.conf | 14 | ||||
-rw-r--r-- | keystone/tests/keystone.domain1.conf | 11 | ||||
-rw-r--r-- | keystone/tests/keystone.domain2.conf | 13 | ||||
-rw-r--r-- | keystone/tests/test_backend.py | 7 | ||||
-rw-r--r-- | keystone/tests/test_backend_ldap.py | 281 | ||||
-rw-r--r-- | keystone/token/backends/memcache.py | 2 | ||||
-rw-r--r-- | keystone/token/core.py | 2 |
22 files changed, 1023 insertions, 412 deletions
diff --git a/keystone/auth/plugins/password.py b/keystone/auth/plugins/password.py index 66c6d05b..b069f4d9 100644 --- a/keystone/auth/plugins/password.py +++ b/keystone/auth/plugins/password.py @@ -94,6 +94,7 @@ class UserAuthInfo(object): self._assert_user_is_enabled(user_ref) self.user_ref = user_ref self.user_id = user_ref['id'] + self.domain_id = domain_ref['id'] class Password(auth.AuthMethodHandler): @@ -106,7 +107,8 @@ class Password(auth.AuthMethodHandler): try: self.identity_api.authenticate( user_id=user_info.user_id, - password=user_info.password) + password=user_info.password, + domain_scope=user_info.domain_id) except AssertionError: # authentication failed because of invalid username or password msg = _('Invalid username or password') diff --git a/keystone/catalog/backends/templated.py b/keystone/catalog/backends/templated.py index 7fe73e91..db99110b 100644 --- a/keystone/catalog/backends/templated.py +++ b/keystone/catalog/backends/templated.py @@ -25,9 +25,6 @@ from keystone.openstack.common import log as logging LOG = logging.getLogger(__name__) CONF = config.CONF -config.register_str('template_file', - default='default_catalog.templates', - group='catalog') def parse_templates(template_lines): diff --git a/keystone/common/config.py b/keystone/common/config.py index 5a961d4a..61eeac92 100644 --- a/keystone/common/config.py +++ b/keystone/common/config.py @@ -24,6 +24,218 @@ _DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" _DEFAULT_AUTH_METHODS = ['external', 'password', 'token'] +FILE_OPTIONS = { + '': [ + cfg.StrOpt('admin_token', secret=True, default='ADMIN'), + cfg.StrOpt('bind_host', default='0.0.0.0'), + cfg.IntOpt('compute_port', default=8774), + cfg.IntOpt('admin_port', default=35357), + cfg.IntOpt('public_port', default=5000), + cfg.StrOpt('public_endpoint', + default='http://localhost:%(public_port)s/'), + cfg.StrOpt('admin_endpoint', + default='http://localhost:%(admin_port)s/'), + cfg.StrOpt('onready'), + cfg.StrOpt('auth_admin_prefix', default=''), + cfg.StrOpt('policy_file', default='policy.json'), + cfg.StrOpt('policy_default_rule', default=None), + # default max request size is 112k + cfg.IntOpt('max_request_body_size', default=114688), + cfg.IntOpt('max_param_size', default=64), + # we allow tokens to be a bit larger to accommodate PKI + cfg.IntOpt('max_token_size', default=8192), + cfg.StrOpt('member_role_id', + default='9fe2ff9ee4384b1894a90878d3e92bab'), + cfg.StrOpt('member_role_name', default='_member_'), + cfg.IntOpt('crypt_strength', default=40000)], + 'identity': [ + cfg.StrOpt('default_domain_id', default='default'), + cfg.BoolOpt('domain_specific_drivers_enabled', + default=False), + cfg.StrOpt('domain_config_dir', + default='/etc/keystone/domains'), + cfg.StrOpt('driver', + default=('keystone.identity.backends' + '.sql.Identity')), + cfg.IntOpt('max_password_length', default=4096)], + 'trust': [ + cfg.BoolOpt('enabled', default=True), + cfg.StrOpt('driver', + default='keystone.trust.backends.sql.Trust')], + 'os_inherit': [ + cfg.BoolOpt('enabled', default=False)], + 'token': [ + cfg.ListOpt('bind', default=[]), + cfg.StrOpt('enforce_token_bind', default='permissive'), + cfg.IntOpt('expiration', default=86400), + cfg.StrOpt('provider', default=None), + cfg.StrOpt('driver', + default='keystone.token.backends.sql.Token')], + 'ssl': [ + cfg.BoolOpt('enable', default=False), + cfg.StrOpt('certfile', + default="/etc/keystone/ssl/certs/keystone.pem"), + cfg.StrOpt('keyfile', + default="/etc/keystone/ssl/private/keystonekey.pem"), + cfg.StrOpt('ca_certs', + default="/etc/keystone/ssl/certs/ca.pem"), + cfg.StrOpt('ca_key', + default="/etc/keystone/ssl/certs/cakey.pem"), + cfg.BoolOpt('cert_required', default=False), + cfg.IntOpt('key_size', default=1024), + cfg.IntOpt('valid_days', default=3650), + cfg.StrOpt('ca_password', default=None), + cfg.StrOpt('cert_subject', + default='/C=US/ST=Unset/L=Unset/O=Unset/CN=localhost')], + 'signing': [ + cfg.StrOpt('token_format', default=None), + cfg.StrOpt('certfile', + default="/etc/keystone/ssl/certs/signing_cert.pem"), + cfg.StrOpt('keyfile', + default="/etc/keystone/ssl/private/signing_key.pem"), + cfg.StrOpt('ca_certs', + default="/etc/keystone/ssl/certs/ca.pem"), + cfg.StrOpt('ca_key', + default="/etc/keystone/ssl/certs/cakey.pem"), + cfg.IntOpt('key_size', default=2048), + cfg.IntOpt('valid_days', default=3650), + cfg.StrOpt('ca_password', default=None), + cfg.StrOpt('cert_subject', + default=('/C=US/ST=Unset/L=Unset/O=Unset/' + 'CN=www.example.com'))], + 'sql': [ + cfg.StrOpt('connection', secret=True, + default='sqlite:///keystone.db'), + cfg.IntOpt('idle_timeout', default=200)], + 'assignment': [ + # assignment has no default for backward compatibility reasons. + # If assignment driver is not specified, the identity driver chooses + # the backend + cfg.StrOpt('driver', default=None)], + 'credential': [ + cfg.StrOpt('driver', + default=('keystone.credential.backends' + '.sql.Credential'))], + 'policy': [ + cfg.StrOpt('driver', + default='keystone.policy.backends.sql.Policy')], + 'ec2': [ + cfg.StrOpt('driver', + default='keystone.contrib.ec2.backends.kvs.Ec2')], + 'stats': [ + cfg.StrOpt('driver', + default=('keystone.contrib.stats.backends' + '.kvs.Stats'))], + 'ldap': [ + cfg.StrOpt('url', default='ldap://localhost'), + cfg.StrOpt('user', default=None), + cfg.StrOpt('password', secret=True, default=None), + cfg.StrOpt('suffix', default='cn=example,cn=com'), + cfg.BoolOpt('use_dumb_member', default=False), + cfg.StrOpt('dumb_member', default='cn=dumb,dc=nonexistent'), + cfg.BoolOpt('allow_subtree_delete', default=False), + cfg.StrOpt('query_scope', default='one'), + cfg.IntOpt('page_size', default=0), + cfg.StrOpt('alias_dereferencing', default='default'), + + cfg.StrOpt('user_tree_dn', default=None), + cfg.StrOpt('user_filter', default=None), + cfg.StrOpt('user_objectclass', default='inetOrgPerson'), + cfg.StrOpt('user_id_attribute', default='cn'), + cfg.StrOpt('user_name_attribute', default='sn'), + cfg.StrOpt('user_mail_attribute', default='email'), + cfg.StrOpt('user_pass_attribute', default='userPassword'), + cfg.StrOpt('user_enabled_attribute', default='enabled'), + cfg.StrOpt('user_domain_id_attribute', + default='businessCategory'), + cfg.IntOpt('user_enabled_mask', default=0), + cfg.StrOpt('user_enabled_default', default='True'), + cfg.ListOpt('user_attribute_ignore', + default='tenant_id,tenants'), + cfg.BoolOpt('user_allow_create', default=True), + cfg.BoolOpt('user_allow_update', default=True), + cfg.BoolOpt('user_allow_delete', default=True), + cfg.BoolOpt('user_enabled_emulation', default=False), + cfg.StrOpt('user_enabled_emulation_dn', default=None), + cfg.ListOpt('user_additional_attribute_mapping', + default=None), + + cfg.StrOpt('tenant_tree_dn', default=None), + cfg.StrOpt('tenant_filter', default=None), + cfg.StrOpt('tenant_objectclass', default='groupOfNames'), + cfg.StrOpt('tenant_id_attribute', default='cn'), + cfg.StrOpt('tenant_member_attribute', default='member'), + cfg.StrOpt('tenant_name_attribute', default='ou'), + cfg.StrOpt('tenant_desc_attribute', default='description'), + cfg.StrOpt('tenant_enabled_attribute', default='enabled'), + cfg.StrOpt('tenant_domain_id_attribute', + default='businessCategory'), + cfg.ListOpt('tenant_attribute_ignore', default=''), + cfg.BoolOpt('tenant_allow_create', default=True), + cfg.BoolOpt('tenant_allow_update', default=True), + cfg.BoolOpt('tenant_allow_delete', default=True), + cfg.BoolOpt('tenant_enabled_emulation', default=False), + cfg.StrOpt('tenant_enabled_emulation_dn', default=None), + cfg.ListOpt('tenant_additional_attribute_mapping', + default=None), + + cfg.StrOpt('role_tree_dn', default=None), + cfg.StrOpt('role_filter', default=None), + cfg.StrOpt('role_objectclass', default='organizationalRole'), + cfg.StrOpt('role_id_attribute', default='cn'), + cfg.StrOpt('role_name_attribute', default='ou'), + cfg.StrOpt('role_member_attribute', default='roleOccupant'), + cfg.ListOpt('role_attribute_ignore', default=''), + cfg.BoolOpt('role_allow_create', default=True), + cfg.BoolOpt('role_allow_update', default=True), + cfg.BoolOpt('role_allow_delete', default=True), + cfg.ListOpt('role_additional_attribute_mapping', + default=None), + + cfg.StrOpt('group_tree_dn', default=None), + cfg.StrOpt('group_filter', default=None), + cfg.StrOpt('group_objectclass', default='groupOfNames'), + cfg.StrOpt('group_id_attribute', default='cn'), + cfg.StrOpt('group_name_attribute', default='ou'), + cfg.StrOpt('group_member_attribute', default='member'), + cfg.StrOpt('group_desc_attribute', default='description'), + cfg.StrOpt('group_domain_id_attribute', + default='businessCategory'), + cfg.ListOpt('group_attribute_ignore', default=''), + cfg.BoolOpt('group_allow_create', default=True), + cfg.BoolOpt('group_allow_update', default=True), + cfg.BoolOpt('group_allow_delete', default=True), + cfg.ListOpt('group_additional_attribute_mapping', + default=None), + + cfg.StrOpt('tls_cacertfile', default=None), + cfg.StrOpt('tls_cacertdir', default=None), + cfg.BoolOpt('use_tls', default=False), + cfg.StrOpt('tls_req_cert', default='demand')], + 'pam': [ + cfg.StrOpt('userid', default=None), + cfg.StrOpt('password', default=None)], + 'auth': [ + cfg.ListOpt('methods', default=_DEFAULT_AUTH_METHODS), + cfg.StrOpt('password', + default='keystone.auth.plugins.token.Token'), + cfg.StrOpt('token', + default='keystone.auth.plugins.password.Password'), + #deals with REMOTE_USER authentication + cfg.StrOpt('external', + default='keystone.auth.plugins.external.ExternalDefault')], + 'paste_deploy': [ + cfg.StrOpt('config_file', default=None)], + 'memcache': [ + cfg.StrOpt('servers', default='localhost:11211'), + cfg.IntOpt('max_compare_and_set_retry', default=16)], + 'catalog': [ + cfg.StrOpt('template_file', + default='default_catalog.templates'), + cfg.StrOpt('driver', + default='keystone.catalog.backends.sql.Catalog')]} + + CONF = cfg.CONF @@ -40,297 +252,35 @@ def setup_logging(conf, product_name='keystone'): logging.setup(product_name) -def setup_authentication(): +def setup_authentication(conf=None): # register any non-default auth methods here (used by extensions, etc) - for method_name in CONF.auth.methods: + if conf is None: + conf = CONF + for method_name in conf.auth.methods: if method_name not in _DEFAULT_AUTH_METHODS: - register_str(method_name, group="auth") - - -def register_str(*args, **kw): - conf = kw.pop('conf', CONF) - group = kw.pop('group', None) - return conf.register_opt(cfg.StrOpt(*args, **kw), group=group) - - -def register_cli_str(*args, **kw): - conf = kw.pop('conf', CONF) - group = kw.pop('group', None) - return conf.register_cli_opt(cfg.StrOpt(*args, **kw), group=group) - - -def register_list(*args, **kw): - conf = kw.pop('conf', CONF) - group = kw.pop('group', None) - return conf.register_opt(cfg.ListOpt(*args, **kw), group=group) - - -def register_cli_list(*args, **kw): - conf = kw.pop('conf', CONF) - group = kw.pop('group', None) - return conf.register_cli_opt(cfg.ListOpt(*args, **kw), group=group) - - -def register_bool(*args, **kw): - conf = kw.pop('conf', CONF) - group = kw.pop('group', None) - return conf.register_opt(cfg.BoolOpt(*args, **kw), group=group) - - -def register_cli_bool(*args, **kw): - conf = kw.pop('conf', CONF) - group = kw.pop('group', None) - return conf.register_cli_opt(cfg.BoolOpt(*args, **kw), group=group) - - -def register_int(*args, **kw): - conf = kw.pop('conf', CONF) - group = kw.pop('group', None) - return conf.register_opt(cfg.IntOpt(*args, **kw), group=group) - - -def register_cli_int(*args, **kw): - conf = kw.pop('conf', CONF) - group = kw.pop('group', None) - return conf.register_cli_opt(cfg.IntOpt(*args, **kw), group=group) - - -def configure(): - register_cli_bool('standard-threads', default=False, - help='Do not monkey-patch threading system modules.') + conf.register_opt(cfg.StrOpt(method_name), group='auth') + + +def configure(conf=None): + if conf is None: + conf = CONF + + conf.register_cli_opt( + cfg.BoolOpt('standard-threads', default=False, + help='Do not monkey-patch threading system modules.')) + conf.register_cli_opt( + cfg.StrOpt('pydev-debug-host', default=None, + help='Host to connect to for remote debugger.')) + conf.register_cli_opt( + cfg.IntOpt('pydev-debug-port', default=None, + help='Port to connect to for remote debugger.')) + + for section in FILE_OPTIONS: + for option in FILE_OPTIONS[section]: + if section: + conf.register_opt(option, group=section) + else: + conf.register_opt(option) - register_cli_str('pydev-debug-host', default=None, - help='Host to connect to for remote debugger.') - register_cli_int('pydev-debug-port', default=None, - help='Port to connect to for remote debugger.') - - register_str('admin_token', secret=True, default='ADMIN') - register_str('bind_host', default='0.0.0.0') - register_int('compute_port', default=8774) - register_int('admin_port', default=35357) - register_int('public_port', default=5000) - register_str( - 'public_endpoint', default='http://localhost:%(public_port)s/') - register_str('admin_endpoint', default='http://localhost:%(admin_port)s/') - register_str('onready') - register_str('auth_admin_prefix', default='') - register_str('policy_file', default='policy.json') - register_str('policy_default_rule', default=None) - # default max request size is 112k - register_int('max_request_body_size', default=114688) - register_int('max_param_size', default=64) - # we allow tokens to be a bit larger to accommodate PKI - register_int('max_token_size', default=8192) - register_str( - 'member_role_id', default='9fe2ff9ee4384b1894a90878d3e92bab') - register_str('member_role_name', default='_member_') - - # identity - register_str('default_domain_id', group='identity', default='default') - register_int('max_password_length', group='identity', default=4096) - - # trust - register_bool('enabled', group='trust', default=True) - - # os_inherit - register_bool('enabled', group='os_inherit', default=False) - - # binding - register_list('bind', group='token', default=[]) - register_str('enforce_token_bind', group='token', default='permissive') - - # ssl - register_bool('enable', group='ssl', default=False) - register_str('certfile', group='ssl', - default="/etc/keystone/ssl/certs/keystone.pem") - register_str('keyfile', group='ssl', - default="/etc/keystone/ssl/private/keystonekey.pem") - register_str('ca_certs', group='ssl', - default="/etc/keystone/ssl/certs/ca.pem") - register_str('ca_key', group='ssl', - default="/etc/keystone/ssl/certs/cakey.pem") - register_bool('cert_required', group='ssl', default=False) - register_int('key_size', group='ssl', default=1024) - register_int('valid_days', group='ssl', default=3650) - register_str('ca_password', group='ssl', default=None) - register_str('cert_subject', group='ssl', - default='/C=US/ST=Unset/L=Unset/O=Unset/CN=localhost') - - # signing - register_str( - 'token_format', group='signing', default=None) - register_str( - 'certfile', - group='signing', - default="/etc/keystone/ssl/certs/signing_cert.pem") - register_str( - 'keyfile', - group='signing', - default="/etc/keystone/ssl/private/signing_key.pem") - register_str( - 'ca_certs', - group='signing', - default="/etc/keystone/ssl/certs/ca.pem") - register_str('ca_key', group='signing', - default="/etc/keystone/ssl/certs/cakey.pem") - register_int('key_size', group='signing', default=2048) - register_int('valid_days', group='signing', default=3650) - register_str('ca_password', group='signing', default=None) - register_str('cert_subject', group='signing', - default='/C=US/ST=Unset/L=Unset/O=Unset/CN=www.example.com') - - # sql - register_str('connection', group='sql', secret=True, - default='sqlite:///keystone.db') - register_int('idle_timeout', group='sql', default=200) - - #assignment has no default for backward compatibility reasons. - #If assignment is not specified, the identity driver chooses the backend - register_str( - 'driver', - group='assignment', - default=None) - register_str( - 'driver', - group='catalog', - default='keystone.catalog.backends.sql.Catalog') - register_str( - 'driver', - group='identity', - default='keystone.identity.backends.sql.Identity') - register_str( - 'driver', - group='credential', - default='keystone.credential.backends.sql.Credential') - register_str( - 'driver', - group='policy', - default='keystone.policy.backends.sql.Policy') - register_str( - 'driver', group='token', default='keystone.token.backends.sql.Token') - register_str( - 'driver', group='trust', default='keystone.trust.backends.sql.Trust') - register_str( - 'driver', group='ec2', default='keystone.contrib.ec2.backends.kvs.Ec2') - register_str( - 'driver', - group='stats', - default='keystone.contrib.stats.backends.kvs.Stats') - - # ldap - register_str('url', group='ldap', default='ldap://localhost') - register_str('user', group='ldap', default=None) - register_str('password', group='ldap', secret=True, default=None) - register_str('suffix', group='ldap', default='cn=example,cn=com') - register_bool('use_dumb_member', group='ldap', default=False) - register_str('dumb_member', group='ldap', default='cn=dumb,dc=nonexistent') - register_bool('allow_subtree_delete', group='ldap', default=False) - register_str('query_scope', group='ldap', default='one') - register_int('page_size', group='ldap', default=0) - register_str('alias_dereferencing', group='ldap', default='default') - - register_str('user_tree_dn', group='ldap', default=None) - register_str('user_filter', group='ldap', default=None) - register_str('user_objectclass', group='ldap', default='inetOrgPerson') - register_str('user_id_attribute', group='ldap', default='cn') - register_str('user_name_attribute', group='ldap', default='sn') - register_str('user_mail_attribute', group='ldap', default='email') - register_str('user_pass_attribute', group='ldap', default='userPassword') - register_str('user_enabled_attribute', group='ldap', default='enabled') - register_str( - 'user_domain_id_attribute', group='ldap', default='businessCategory') - register_int('user_enabled_mask', group='ldap', default=0) - register_str('user_enabled_default', group='ldap', default='True') - register_list( - 'user_attribute_ignore', group='ldap', default='tenant_id,tenants') - register_bool('user_allow_create', group='ldap', default=True) - register_bool('user_allow_update', group='ldap', default=True) - register_bool('user_allow_delete', group='ldap', default=True) - register_bool('user_enabled_emulation', group='ldap', default=False) - register_str('user_enabled_emulation_dn', group='ldap', default=None) - register_list( - 'user_additional_attribute_mapping', group='ldap', default=None) - - register_str('tenant_tree_dn', group='ldap', default=None) - register_str('tenant_filter', group='ldap', default=None) - register_str('tenant_objectclass', group='ldap', default='groupOfNames') - register_str('tenant_id_attribute', group='ldap', default='cn') - register_str('tenant_member_attribute', group='ldap', default='member') - register_str('tenant_name_attribute', group='ldap', default='ou') - register_str('tenant_desc_attribute', group='ldap', default='description') - register_str('tenant_enabled_attribute', group='ldap', default='enabled') - register_str( - 'tenant_domain_id_attribute', group='ldap', default='businessCategory') - register_list('tenant_attribute_ignore', group='ldap', default='') - register_bool('tenant_allow_create', group='ldap', default=True) - register_bool('tenant_allow_update', group='ldap', default=True) - register_bool('tenant_allow_delete', group='ldap', default=True) - register_bool('tenant_enabled_emulation', group='ldap', default=False) - register_str('tenant_enabled_emulation_dn', group='ldap', default=None) - register_list( - 'tenant_additional_attribute_mapping', group='ldap', default=None) - - register_str('role_tree_dn', group='ldap', default=None) - register_str('role_filter', group='ldap', default=None) - register_str( - 'role_objectclass', group='ldap', default='organizationalRole') - register_str('role_id_attribute', group='ldap', default='cn') - register_str('role_name_attribute', group='ldap', default='ou') - register_str('role_member_attribute', group='ldap', default='roleOccupant') - register_list('role_attribute_ignore', group='ldap', default='') - register_bool('role_allow_create', group='ldap', default=True) - register_bool('role_allow_update', group='ldap', default=True) - register_bool('role_allow_delete', group='ldap', default=True) - register_list( - 'role_additional_attribute_mapping', group='ldap', default=None) - - register_str('group_tree_dn', group='ldap', default=None) - register_str('group_filter', group='ldap', default=None) - register_str('group_objectclass', group='ldap', default='groupOfNames') - register_str('group_id_attribute', group='ldap', default='cn') - register_str('group_name_attribute', group='ldap', default='ou') - register_str('group_member_attribute', group='ldap', default='member') - register_str('group_desc_attribute', group='ldap', default='description') - register_str( - 'group_domain_id_attribute', group='ldap', default='businessCategory') - register_list('group_attribute_ignore', group='ldap', default='') - register_bool('group_allow_create', group='ldap', default=True) - register_bool('group_allow_update', group='ldap', default=True) - register_bool('group_allow_delete', group='ldap', default=True) - register_list( - 'group_additional_attribute_mapping', group='ldap', default=None) - - register_str('tls_cacertfile', group='ldap', default=None) - register_str('tls_cacertdir', group='ldap', default=None) - register_bool('use_tls', group='ldap', default=False) - register_str('tls_req_cert', group='ldap', default='demand') - - # pam - register_str('userid', group='pam', default=None) - register_str('password', group='pam', default=None) - - # default authentication methods - register_list('methods', group='auth', default=_DEFAULT_AUTH_METHODS) - register_str( - 'password', group='auth', default='keystone.auth.plugins.token.Token') - register_str( - 'token', group='auth', - default='keystone.auth.plugins.password.Password') - #deals with REMOTE_USER authentication - register_str( - 'external', - group='auth', - default='keystone.auth.plugins.external.ExternalDefault') # register any non-default auth methods here (used by extensions, etc) - for method_name in CONF.auth.methods: - if method_name not in _DEFAULT_AUTH_METHODS: - register_str(method_name, group='auth') - - # PasteDeploy config file - register_str('config_file', group='paste_deploy', default=None) - - # token provider - register_str( - 'provider', - group='token', - default=None) + setup_authentication(conf) diff --git a/keystone/common/controller.py b/keystone/common/controller.py index 1bf65cda..90818fb4 100644 --- a/keystone/common/controller.py +++ b/keystone/common/controller.py @@ -303,34 +303,35 @@ class V3Controller(V2Controller): ref['id'] = uuid.uuid4().hex return ref + def _get_domain_id_for_request(self, context): + """Get the domain_id for a v3 call.""" + + if context['is_admin']: + return DEFAULT_DOMAIN_ID + + # Fish the domain_id out of the token + # + # We could make this more efficient by loading the domain_id + # into the context in the wrapper function above (since + # this version of normalize_domain will only be called inside + # a v3 protected call). However, this optimization is probably not + # worth the duplication of state + try: + token_ref = self.token_api.get_token( + token_id=context['token_id']) + except exception.TokenNotFound: + LOG.warning(_('Invalid token in _get_domain_id_for_request')) + raise exception.Unauthorized() + + if 'domain' in token_ref: + return token_ref['domain']['id'] + else: + return DEFAULT_DOMAIN_ID + def _normalize_domain_id(self, context, ref): """Fill in domain_id if not specified in a v3 call.""" - if 'domain_id' not in ref: - if context['is_admin']: - ref['domain_id'] = DEFAULT_DOMAIN_ID - else: - # Fish the domain_id out of the token - # - # We could make this more efficient by loading the domain_id - # into the context in the wrapper function above (since - # this version of normalize_domain will only be called inside - # a v3 protected call). However, given that we only use this - # for creating entities, this optimization is probably not - # worth the duplication of state - try: - token_ref = self.token_api.get_token( - token_id=context['token_id']) - except exception.TokenNotFound: - LOG.warning(_('Invalid token in normalize_domain_id')) - raise exception.Unauthorized() - - if 'domain' in token_ref: - ref['domain_id'] = token_ref['domain']['id'] - else: - # FIXME(henry-nash) Revisit this once v3 token scoping - # across domains has been hashed out - ref['domain_id'] = DEFAULT_DOMAIN_ID + ref['domain_id'] = self._get_domain_id_for_request(context) return ref def _filter_domain_id(self, ref): diff --git a/keystone/common/ldap/fakeldap.py b/keystone/common/ldap/fakeldap.py index c19e1355..e4458874 100644 --- a/keystone/common/ldap/fakeldap.py +++ b/keystone/common/ldap/fakeldap.py @@ -123,18 +123,14 @@ server_fail = False class FakeShelve(dict): - @classmethod - def get_instance(cls): - try: - return cls.__instance - except AttributeError: - cls.__instance = cls() - return cls.__instance def sync(self): pass +FakeShelves = {} + + class FakeLdap(object): """Fake LDAP connection.""" @@ -142,8 +138,10 @@ class FakeLdap(object): def __init__(self, url): LOG.debug(_('FakeLdap initialize url=%s'), url) - if url == 'fake://memory': - self.db = FakeShelve.get_instance() + if url.startswith('fake://memory'): + if url not in FakeShelves: + FakeShelves[url] = FakeShelve() + self.db = FakeShelves[url] else: self.db = shelve.open(url[7:]) diff --git a/keystone/common/utils.py b/keystone/common/utils.py index 4abad57a..27968efc 100644 --- a/keystone/common/utils.py +++ b/keystone/common/utils.py @@ -32,7 +32,6 @@ from keystone.openstack.common import log as logging CONF = config.CONF -config.register_int('crypt_strength', default=40000) LOG = logging.getLogger(__name__) diff --git a/keystone/config.py b/keystone/config.py index 28f1cf2c..c4a43b47 100644 --- a/keystone/config.py +++ b/keystone/config.py @@ -25,15 +25,8 @@ config.configure() CONF = config.CONF setup_logging = config.setup_logging -register_str = config.register_str -register_cli_str = config.register_cli_str -register_list = config.register_list -register_cli_list = config.register_cli_list -register_bool = config.register_bool -register_cli_bool = config.register_cli_bool -register_int = config.register_int -register_cli_int = config.register_cli_int setup_authentication = config.setup_authentication +configure = config.configure def find_paste_config(): diff --git a/keystone/identity/backends/kvs.py b/keystone/identity/backends/kvs.py index 0323d3d0..bcfb777b 100644 --- a/keystone/identity/backends/kvs.py +++ b/keystone/identity/backends/kvs.py @@ -27,6 +27,9 @@ class Identity(kvs.Base, identity.Driver): def default_assignment_driver(self): return "keystone.assignment.backends.kvs.Assignment" + def is_domain_aware(self): + return True + # Public interface def authenticate(self, user_id, password): user_ref = None diff --git a/keystone/identity/backends/ldap.py b/keystone/identity/backends/ldap.py index ef3b5d61..67380f6e 100644 --- a/keystone/identity/backends/ldap.py +++ b/keystone/identity/backends/ldap.py @@ -41,14 +41,19 @@ DEFAULT_DOMAIN = { @dependency.requires('assignment_api') class Identity(identity.Driver): - def __init__(self): + def __init__(self, conf=None): super(Identity, self).__init__() - self.user = UserApi(CONF) - self.group = GroupApi(CONF) + if conf is None: + conf = CONF + self.user = UserApi(conf) + self.group = GroupApi(conf) def default_assignment_driver(self): return "keystone.assignment.backends.ldap.Assignment" + def is_domain_aware(self): + return False + # Identity interface def create_project(self, project_id, project): @@ -68,37 +73,31 @@ class Identity(identity.Driver): raise AssertionError('Invalid user / password') except Exception: raise AssertionError('Invalid user / password') - return self.assignment_api._set_default_domain( - identity.filter_user(user_ref)) + return identity.filter_user(user_ref) def _get_user(self, user_id): return self.user.get(user_id) def get_user(self, user_id): - ref = identity.filter_user(self._get_user(user_id)) - return self.assignment_api._set_default_domain(ref) + return identity.filter_user(self._get_user(user_id)) def list_users(self): - return (self.assignment_api._set_default_domain - (self.user.get_all_filtered())) + return self.user.get_all_filtered() def get_user_by_name(self, user_name, domain_id): - self.assignment_api._validate_default_domain_id(domain_id) - ref = identity.filter_user(self.user.get_by_name(user_name)) - return self.assignment_api._set_default_domain(ref) + # domain_id will already have been handled in the Manager layer, + # parameter left in so this matches the Driver specification + return identity.filter_user(self.user.get_by_name(user_name)) # CRUD def create_user(self, user_id, user): - user = self.assignment_api._validate_default_domain(user) user_ref = self.user.create(user) tenant_id = user.get('tenant_id') if tenant_id is not None: self.assignment_api.add_user_to_project(tenant_id, user_id) - return (self.assignment_api._set_default_domain - (identity.filter_user(user_ref))) + return identity.filter_user(user_ref) def update_user(self, user_id, user): - user = self.assignment_api._validate_default_domain(user) if 'id' in user and user['id'] != user_id: raise exception.ValidationError('Cannot change user ID') old_obj = self.user.get(user_id) @@ -121,8 +120,7 @@ class Identity(identity.Driver): user['enabled_nomask'] = old_obj['enabled_nomask'] self.user.mask_enabled_attribute(user) self.user.update(user_id, user, old_obj) - return (self.assignment_api._set_default_domain - (self.user.get_filtered(user_id))) + return self.user.get_filtered(user_id) def delete_user(self, user_id): self.assignment_api.delete_user(user_id) @@ -138,21 +136,16 @@ class Identity(identity.Driver): self.user.delete(user_id) def create_group(self, group_id, group): - group = self.assignment_api._validate_default_domain(group) group['name'] = clean.group_name(group['name']) - return self.assignment_api._set_default_domain( - self.group.create(group)) + return self.group.create(group) def get_group(self, group_id): - return self.assignment_api._set_default_domain( - self.group.get(group_id)) + return self.group.get(group_id) def update_group(self, group_id, group): - group = self.assignment_api._validate_default_domain(group) if 'name' in group: group['name'] = clean.group_name(group['name']) - return (self.assignment_api._set_default_domain - (self.group.update(group_id, group))) + return self.group.update(group_id, group) def delete_group(self, group_id): return self.group.delete(group_id) @@ -172,11 +165,10 @@ class Identity(identity.Driver): def list_groups_for_user(self, user_id): self.get_user(user_id) user_dn = self.user._id_to_dn(user_id) - return (self.assignment_api._set_default_domain - (self.group.list_user_groups(user_dn))) + return self.group.list_user_groups(user_dn) def list_groups(self): - return self.assignment_api._set_default_domain(self.group.get_all()) + return self.group.get_all() def list_users_in_group(self, group_id): self.get_group(group_id) @@ -190,7 +182,7 @@ class Identity(identity.Driver): " '%(group_id)s'. The user should be removed" " from the group. The user will be ignored.") % dict(user_dn=user_dn, group_id=group_id)) - return self.assignment_api._set_default_domain(users) + return users def check_user_in_group(self, user_id, group_id): self.get_user(user_id) diff --git a/keystone/identity/backends/pam.py b/keystone/identity/backends/pam.py index 2a6ee621..a5459694 100644 --- a/keystone/identity/backends/pam.py +++ b/keystone/identity/backends/pam.py @@ -58,6 +58,9 @@ class PamIdentity(identity.Driver): Tenant is always the same as User, root user has admin role. """ + def is_domain_aware(self): + return False + def authenticate(self, user_id, password): auth = pam.authenticate if pam else PAM_authenticate if not auth(user_id, password): diff --git a/keystone/identity/backends/sql.py b/keystone/identity/backends/sql.py index 65a34a8a..84026a58 100644 --- a/keystone/identity/backends/sql.py +++ b/keystone/identity/backends/sql.py @@ -85,6 +85,9 @@ class Identity(sql.Base, identity.Driver): """ return utils.check_password(password, user_ref.password) + def is_domain_aware(self): + return True + # Identity interface def authenticate(self, user_id, password): session = self.get_session() diff --git a/keystone/identity/controllers.py b/keystone/identity/controllers.py index 67f3beac..281e3f1b 100644 --- a/keystone/identity/controllers.py +++ b/keystone/identity/controllers.py @@ -620,23 +620,30 @@ class UserV3(controller.V3Controller): @controller.filterprotected('domain_id', 'email', 'enabled', 'name') def list_users(self, context, filters): - refs = self.identity_api.list_users() + refs = self.identity_api.list_users( + domain_scope=self._get_domain_id_for_request(context)) return UserV3.wrap_collection(context, refs, filters) @controller.filterprotected('domain_id', 'email', 'enabled', 'name') def list_users_in_group(self, context, filters, group_id): - refs = self.identity_api.list_users_in_group(group_id) + refs = self.identity_api.list_users_in_group( + group_id, + domain_scope=self._get_domain_id_for_request(context)) return UserV3.wrap_collection(context, refs, filters) @controller.protected def get_user(self, context, user_id): - ref = self.identity_api.get_user(user_id) + ref = self.identity_api.get_user( + user_id, + domain_scope=self._get_domain_id_for_request(context)) return UserV3.wrap_member(context, ref) @controller.protected def update_user(self, context, user_id, user): self._require_matching_id(user_id, user) - ref = self.identity_api.update_user(user_id, user) + ref = self.identity_api.update_user( + user_id, user, + domain_scope=self._get_domain_id_for_request(context)) if user.get('password') or not user.get('enabled', True): # revoke all tokens owned by this user @@ -646,18 +653,24 @@ class UserV3(controller.V3Controller): @controller.protected def add_user_to_group(self, context, user_id, group_id): - self.identity_api.add_user_to_group(user_id, group_id) + self.identity_api.add_user_to_group( + user_id, group_id, + domain_scope=self._get_domain_id_for_request(context)) # Delete any tokens so that group membership can have an # immediate effect self._delete_tokens_for_user(user_id) @controller.protected def check_user_in_group(self, context, user_id, group_id): - return self.identity_api.check_user_in_group(user_id, group_id) + return self.identity_api.check_user_in_group( + user_id, group_id, + domain_scope=self._get_domain_id_for_request(context)) @controller.protected def remove_user_from_group(self, context, user_id, group_id): - self.identity_api.remove_user_from_group(user_id, group_id) + self.identity_api.remove_user_from_group( + user_id, group_id, + domain_scope=self._get_domain_id_for_request(context)) self._delete_tokens_for_user(user_id) def _delete_user(self, context, user_id): @@ -667,11 +680,13 @@ class UserV3(controller.V3Controller): self.credential_api.delete_credential(cred['id']) # Make sure any tokens are marked as deleted + domain_id = self._get_domain_id_for_request(context) self._delete_tokens_for_user(user_id) # Finally delete the user itself - the backend is # responsible for deleting any role assignments related # to this user - return self.identity_api.delete_user(user_id) + return self.identity_api.delete_user( + user_id, domain_scope=domain_id) @controller.protected def delete_user(self, context, user_id): @@ -693,24 +708,31 @@ class GroupV3(controller.V3Controller): @controller.filterprotected('domain_id', 'name') def list_groups(self, context, filters): - refs = self.identity_api.list_groups() + refs = self.identity_api.list_groups( + domain_scope=self._get_domain_id_for_request(context)) return GroupV3.wrap_collection(context, refs, filters) @controller.filterprotected('name') def list_groups_for_user(self, context, filters, user_id): - refs = self.identity_api.list_groups_for_user(user_id) + refs = self.identity_api.list_groups_for_user( + user_id, + domain_scope=self._get_domain_id_for_request(context)) return GroupV3.wrap_collection(context, refs, filters) @controller.protected def get_group(self, context, group_id): - ref = self.identity_api.get_group(group_id) + ref = self.identity_api.get_group( + group_id, + domain_scope=self._get_domain_id_for_request(context)) return GroupV3.wrap_member(context, ref) @controller.protected def update_group(self, context, group_id, group): self._require_matching_id(group_id, group) - ref = self.identity_api.update_group(group_id, group) + ref = self.identity_api.update_group( + group_id, group, + domain_scope=self._get_domain_id_for_request(context)) return GroupV3.wrap_member(context, ref) def _delete_group(self, context, group_id): @@ -720,8 +742,10 @@ class GroupV3(controller.V3Controller): # deletion, so that we can remove these tokens after we know # the group deletion succeeded. - user_refs = self.identity_api.list_users_in_group(group_id) - self.identity_api.delete_group(group_id) + domain_id = self._get_domain_id_for_request(context) + user_refs = self.identity_api.list_users_in_group( + group_id, domain_scope=domain_id) + self.identity_api.delete_group(group_id, domain_scope=domain_id) for user in user_refs: self._delete_tokens_for_user(user['id']) diff --git a/keystone/identity/core.py b/keystone/identity/core.py index 7fb630e2..7d5882e3 100644 --- a/keystone/identity/core.py +++ b/keystone/identity/core.py @@ -16,11 +16,17 @@ """Main entry point into the Identity service.""" +import functools +import os + +from oslo.config import cfg + from keystone import clean from keystone.common import dependency from keystone.common import manager from keystone import config from keystone import exception +from keystone.openstack.common import importutils from keystone.openstack.common import log as logging @@ -51,6 +57,121 @@ def filter_user(user_ref): return user_ref +class DomainConfigs(dict): + """Discover, store and provide access to domain specifc configs. + + The setup_domain_drives() call will be made via the wrapper from + the first call to any driver function handled by this manager. This + setup call it will scan the domain config directory for files of the form + + keystone.<domain_name>.conf + + For each file, the domain_name will be turned into a domain_id and then + this class will: + - Create a new config structure, adding in the specific additional options + defined in this config file + - Initialise a new instance of the required driver with this new config. + + """ + configured = False + driver = None + + def _load_driver(self, assignment_api, domain_id): + domain_config = self[domain_id] + domain_config['driver'] = ( + importutils.import_object( + domain_config['cfg'].identity.driver, domain_config['cfg'])) + domain_config['driver'].assignment_api = assignment_api + + def _load_config(self, assignment_api, file_list, domain_name): + try: + domain_ref = assignment_api.get_domain_by_name(domain_name) + except exception.DomainNotFound: + msg = (_('Invalid domain name (%s) found in config file name') + % domain_name) + LOG.warning(msg) + + if domain_ref: + # Create a new entry in the domain config dict, which contains + # a new instance of both the conf environment and driver using + # options defined in this set of config files. Later, when we + # service calls via this Manager, we'll index via this domain + # config dict to make sure we call the right driver + domain = domain_ref['id'] + self[domain] = {} + self[domain]['cfg'] = cfg.ConfigOpts() + config.configure(conf=self[domain]['cfg']) + self[domain]['cfg'](args=[], project='keystone', + default_config_files=file_list) + self._load_driver(assignment_api, domain) + + def setup_domain_drivers(self, standard_driver, assignment_api): + # This is called by the api call wrapper + self.configured = True + self.driver = standard_driver + + conf_dir = CONF.identity.domain_config_dir + if not os.path.exists(conf_dir): + msg = _('Unable to locate domain config directory: %s') % conf_dir + LOG.warning(msg) + return + + for r, d, f in os.walk(conf_dir): + for file in f: + if file.startswith('keystone.') and file.endswith('.conf'): + names = file.split('.') + if len(names) == 3: + self._load_config(assignment_api, + [os.path.join(r, file)], + names[1]) + else: + msg = (_('Ignoring file (%s) while scanning domain ' + 'config directory') % file) + LOG.debug(msg) + + def get_domain_driver(self, domain_id): + if domain_id in self: + return self[domain_id]['driver'] + + def get_domain_conf(self, domain_id): + if domain_id in self: + return self[domain_id]['cfg'] + + def reload_domain_driver(self, assignment_api, domain_id): + # Only used to support unit tests that want to set + # new config values. This should only be called once + # the domains have been configured, since it relies on + # the fact that the configuration files have already been + # read. + if self.configured: + if domain_id in self: + self._load_driver(assignment_api, domain_id) + else: + # The standard driver + self.driver = self.driver() + self.driver.assignment_api = assignment_api + + +def domains_configured(f): + """Wraps API calls to lazy load domain configs after init. + + This is required since the assignment manager needs to be initialized + before this manager, and yet this manager's init wants to be + able to make assignment calls (to build the domain configs). So + instead, we check if the domains have been initialized on entry + to each call, and if requires load them, + + """ + @functools.wraps(f) + def wrapper(self, *args, **kwargs): + if (not self.domain_configs.configured and + CONF.identity.domain_specific_drivers_enabled): + self.domain_configs.setup_domain_drivers( + self.driver, self.assignment_api) + return f(self, *args, **kwargs) + return wrapper + + @dependency.provider('identity_api') @dependency.requires('assignment_api') class Manager(manager.Manager): @@ -59,30 +180,228 @@ class Manager(manager.Manager): See :mod:`keystone.common.manager.Manager` for more details on how this dynamically calls the backend. + This class also handles the support of domain specific backends, by using + the DomainConfigs class. The setup call for DomainConfigs is called + from with the @domains_configured wrapper in a lazy loading fashion + to get around the fact that we can't satisfy the assignment api it needs + from within our __init__() function since the assignment driver is not + itself yet intitalized. + + Each of the identity calls are pre-processed here to choose, based on + domain, which of the drivers should be called. The non-domain-specific + driver is still in place, and is used if there is no specific driver for + the domain in question. + """ def __init__(self): super(Manager, self).__init__(CONF.identity.driver) - + self.domain_configs = DomainConfigs() + + # Domain ID normalization methods + + def _set_domain_id(self, ref, domain_id): + if isinstance(ref, dict): + ref = ref.copy() + ref['domain_id'] = domain_id + return ref + elif isinstance(ref, list): + return [self._set_domain_id(x, domain_id) for x in ref] + else: + raise ValueError(_('Expected dict or list: %s') % type(ref)) + + def _clear_domain_id(self, ref): + # Clear the domain_id, and then check to ensure that if this + # was not the default domain, it is being handled by its own + # backend driver. + ref = ref.copy() + domain_id = ref.pop('domain_id', CONF.identity.default_domain_id) + if (domain_id != CONF.identity.default_domain_id and + domain_id not in self.domain_configs): + raise exception.DomainNotFound(domain_id=domain_id) + return ref + + def _normalize_scope(self, domain_scope): + if domain_scope is None: + return CONF.identity.default_domain_id + else: + return domain_scope + + def _select_identity_driver(self, domain_id): + driver = self.domain_configs.get_domain_driver(domain_id) + if driver: + return driver + else: + return self.driver + + def _get_domain_conf(self, domain_id): + conf = self.domain_configs.get_domain_conf(domain_id) + if conf: + return conf + else: + return CONF + + def _get_domain_id_and_driver(self, domain_scope): + domain_id = self._normalize_scope(domain_scope) + driver = self._select_identity_driver(domain_id) + return (domain_id, driver) + + # The actual driver calls - these are pre/post processed here as + # part of the Manager layer to make sure we: + # + # - select the right driver for this domain + # - clear/set domain_ids for drivers that do not support domains + + @domains_configured + def authenticate(self, user_id, password, domain_scope=None): + domain_id, driver = self._get_domain_id_and_driver(domain_scope) + ref = driver.authenticate(user_id, password) + if not driver.is_domain_aware(): + ref = self._set_domain_id(ref, domain_id) + return ref + + @domains_configured def create_user(self, user_id, user_ref): user = user_ref.copy() user['name'] = clean.user_name(user['name']) user.setdefault('enabled', True) user['enabled'] = clean.user_enabled(user['enabled']) - return self.driver.create_user(user_id, user) - def update_user(self, user_id, user_ref): + # For creating a user, the domain is in the object itself + domain_id = user_ref['domain_id'] + driver = self._select_identity_driver(domain_id) + if not driver.is_domain_aware(): + user = self._clear_domain_id(user) + ref = driver.create_user(user_id, user) + if not driver.is_domain_aware(): + ref = self._set_domain_id(ref, domain_id) + return ref + + @domains_configured + def get_user(self, user_id, domain_scope=None): + domain_id, driver = self._get_domain_id_and_driver(domain_scope) + ref = driver.get_user(user_id) + if not driver.is_domain_aware(): + ref = self._set_domain_id(ref, domain_id) + return ref + + @domains_configured + def get_user_by_name(self, user_name, domain_id): + driver = self._select_identity_driver(domain_id) + ref = driver.get_user_by_name(user_name, domain_id) + if not driver.is_domain_aware(): + ref = self._set_domain_id(ref, domain_id) + return ref + + @domains_configured + def list_users(self, domain_scope=None): + domain_id, driver = self._get_domain_id_and_driver(domain_scope) + user_list = driver.list_users() + if not driver.is_domain_aware(): + user_list = self._set_domain_id(user_list, domain_id) + return user_list + + @domains_configured + def update_user(self, user_id, user_ref, domain_scope=None): user = user_ref.copy() if 'name' in user: user['name'] = clean.user_name(user['name']) if 'enabled' in user: user['enabled'] = clean.user_enabled(user['enabled']) - return self.driver.update_user(user_id, user) + domain_id, driver = self._get_domain_id_and_driver(domain_scope) + if not driver.is_domain_aware(): + user = self._clear_domain_id(user) + ref = driver.update_user(user_id, user) + if not driver.is_domain_aware(): + ref = self._set_domain_id(ref, domain_id) + return ref + + @domains_configured + def delete_user(self, user_id, domain_scope=None): + domain_id, driver = self._get_domain_id_and_driver(domain_scope) + driver.delete_user(user_id) + + @domains_configured def create_group(self, group_id, group_ref): group = group_ref.copy() group.setdefault('description', '') - return self.driver.create_group(group_id, group) + + # For creating a group, the domain is in the object itself + domain_id = group_ref['domain_id'] + driver = self._select_identity_driver(domain_id) + if not driver.is_domain_aware(): + group = self._clear_domain_id(group) + ref = driver.create_group(group_id, group) + if not driver.is_domain_aware(): + ref = self._set_domain_id(ref, domain_id) + return ref + + @domains_configured + def get_group(self, group_id, domain_scope=None): + domain_id, driver = self._get_domain_id_and_driver(domain_scope) + ref = driver.get_group(group_id) + if not driver.is_domain_aware(): + ref = self._set_domain_id(ref, domain_id) + return ref + + @domains_configured + def update_group(self, group_id, group, domain_scope=None): + domain_id, driver = self._get_domain_id_and_driver(domain_scope) + if not driver.is_domain_aware(): + group = self._clear_domain_id(group) + ref = driver.update_group(group_id, group) + if not driver.is_domain_aware(): + ref = self._set_domain_id(ref, domain_id) + return ref + + @domains_configured + def delete_group(self, group_id, domain_scope=None): + domain_id, driver = self._get_domain_id_and_driver(domain_scope) + driver.delete_group(group_id) + + @domains_configured + def add_user_to_group(self, user_id, group_id, domain_scope=None): + domain_id, driver = self._get_domain_id_and_driver(domain_scope) + driver.add_user_to_group(user_id, group_id) + + @domains_configured + def remove_user_from_group(self, user_id, group_id, domain_scope=None): + domain_id, driver = self._get_domain_id_and_driver(domain_scope) + driver.remove_user_from_group(user_id, group_id) + + @domains_configured + def list_groups_for_user(self, user_id, domain_scope=None): + domain_id, driver = self._get_domain_id_and_driver(domain_scope) + group_list = driver.list_groups_for_user(user_id) + if not driver.is_domain_aware(): + group_list = self._set_domain_id(group_list, domain_id) + return group_list + + @domains_configured + def list_groups(self, domain_scope=None): + domain_id, driver = self._get_domain_id_and_driver(domain_scope) + group_list = driver.list_groups() + if not driver.is_domain_aware(): + group_list = self._set_domain_id(group_list, domain_id) + return group_list + + @domains_configured + def list_users_in_group(self, group_id, domain_scope=None): + domain_id, driver = self._get_domain_id_and_driver(domain_scope) + user_list = driver.list_users_in_group(group_id) + if not driver.is_domain_aware(): + user_list = self._set_domain_id(user_list, domain_id) + return user_list + + @domains_configured + def check_user_in_group(self, user_id, group_id, domain_scope=None): + domain_id, driver = self._get_domain_id_and_driver(domain_scope) + return driver.check_user_in_group(user_id, group_id) + + # TODO(henry-nash, ayoung) The following cross calls to the assignment + # API should be removed, with the controller and tests making the correct + # calls direct to assignment. def create_project(self, tenant_id, tenant_ref): tenant = tenant_ref.copy() @@ -358,6 +677,8 @@ class Driver(object): """ raise exception.NotImplemented() - #end of identity + def is_domain_aware(self): + """Indicates if Driver supports domains.""" + raise exception.NotImplemented() - # Assignments + #end of identity diff --git a/keystone/tests/backend_multi_ldap_sql.conf b/keystone/tests/backend_multi_ldap_sql.conf new file mode 100644 index 00000000..59cff761 --- /dev/null +++ b/keystone/tests/backend_multi_ldap_sql.conf @@ -0,0 +1,35 @@ +[sql] +connection = sqlite:// +#For a file based sqlite use +#connection = sqlite:////tmp/keystone.db +#To Test MySQL: +#connection = mysql://keystone:keystone@localhost/keystone?charset=utf8 +#To Test PostgreSQL: +#connection = postgresql://keystone:keystone@localhost/keystone?client_encoding=utf8 +idle_timeout = 200 + +[identity] +# common identity backend is SQL, domain specific configs will +# set their backends to ldap +driver = keystone.identity.backends.sql.Identity +# The test setup will set this to True, to allow easier creation +# of initial domain data +# domain_specific_drivers_enabled = True + +[assignment] +driver = keystone.assignment.backends.sql.Assignment + +[token] +driver = keystone.token.backends.sql.Token + +[ec2] +driver = keystone.contrib.ec2.backends.sql.Ec2 + +[catalog] +driver = keystone.catalog.backends.sql.Catalog + +[policy] +driver = keystone.policy.backends.sql.Policy + +[trust] +driver = keystone.trust.backends.sql.Trust diff --git a/keystone/tests/core.py b/keystone/tests/core.py index 8d075335..b42a8709 100644 --- a/keystone/tests/core.py +++ b/keystone/tests/core.py @@ -292,9 +292,11 @@ class TestCase(NoModule, unittest.TestCase): for domain in fixtures.DOMAINS: try: rv = self.identity_api.create_domain(domain['id'], domain) - except (exception.Conflict, exception.NotImplemented): - pass - setattr(self, 'domain_%s' % domain['id'], domain) + except exception.Conflict: + rv = self.identity_api.get_domain(domain['id']) + except exception.NotImplemented: + rv = domain + setattr(self, 'domain_%s' % domain['id'], rv) for tenant in fixtures.TENANTS: try: diff --git a/keystone/tests/keystone.Default.conf b/keystone/tests/keystone.Default.conf new file mode 100644 index 00000000..7049afed --- /dev/null +++ b/keystone/tests/keystone.Default.conf @@ -0,0 +1,14 @@ +# The domain-specific configuration file for the default domain for +# use with unit tests. +# +# The domain_name of the default domain is 'Default', hence the +# strange mix of upper/lower case in the file name. + +[ldap] +url = fake://memory +user = cn=Admin +password = password +suffix = cn=example,cn=com + +[identity] +driver = keystone.identity.backends.ldap.Identity
\ No newline at end of file diff --git a/keystone/tests/keystone.domain1.conf b/keystone/tests/keystone.domain1.conf new file mode 100644 index 00000000..6b7e2488 --- /dev/null +++ b/keystone/tests/keystone.domain1.conf @@ -0,0 +1,11 @@ +# The domain-specific configuration file for the test domain +# 'domain1' for use with unit tests. + +[ldap] +url = fake://memory1 +user = cn=Admin +password = password +suffix = cn=example,cn=com + +[identity] +driver = keystone.identity.backends.ldap.Identity
\ No newline at end of file diff --git a/keystone/tests/keystone.domain2.conf b/keystone/tests/keystone.domain2.conf new file mode 100644 index 00000000..0ed68eb9 --- /dev/null +++ b/keystone/tests/keystone.domain2.conf @@ -0,0 +1,13 @@ +# The domain-specific configuration file for the test domain +# 'domain2' for use with unit tests. + +[ldap] +url = fake://memory +user = cn=Admin +password = password +suffix = cn=myroot,cn=com +group_tree_dn = ou=UserGroups,dc=myroot,dc=org +user_tree_dn = ou=Users,dc=myroot,dc=org + +[identity] +driver = keystone.identity.backends.ldap.Identity
\ No newline at end of file diff --git a/keystone/tests/test_backend.py b/keystone/tests/test_backend.py index 52628985..8013deec 100644 --- a/keystone/tests/test_backend.py +++ b/keystone/tests/test_backend.py @@ -105,7 +105,9 @@ class IdentityTests(object): self.assertIn(CONF.member_role_id, role_list) def test_password_hashed(self): - user_ref = self.identity_api._get_user(self.user_foo['id']) + driver = self.identity_api._select_identity_driver( + self.user_foo['domain_id']) + user_ref = driver._get_user(self.user_foo['id']) self.assertNotEqual(user_ref['password'], self.user_foo['password']) def test_create_unicode_user_name(self): @@ -1521,7 +1523,8 @@ class IdentityTests(object): self.assertRaises(exception.UserNotFound, self.identity_api.update_user, user_id, - {'id': user_id}) + {'id': user_id, + 'domain_id': DEFAULT_DOMAIN_ID}) def test_delete_user_with_project_association(self): user = {'id': uuid.uuid4().hex, diff --git a/keystone/tests/test_backend_ldap.py b/keystone/tests/test_backend_ldap.py index 6f9cfef9..e40e0565 100644 --- a/keystone/tests/test_backend_ldap.py +++ b/keystone/tests/test_backend_ldap.py @@ -38,8 +38,16 @@ class BaseLDAPIdentity(test_backend.IdentityTests): return self.identity_api.get_domain(CONF.identity.default_domain_id) def clear_database(self): - db = fakeldap.FakeShelve().get_instance() - db.clear() + for shelf in fakeldap.FakeShelves: + fakeldap.FakeShelves[shelf].clear() + + def reload_backends(self, domain_id): + # Only one backend unless we are using separate domain backends + self.load_backends() + + def get_config(self, domain_id): + # Only one conf structure unless we are using separate domain backends + return CONF def _set_config(self): self.config([test.etcdir('keystone.conf.sample'), @@ -57,6 +65,7 @@ class BaseLDAPIdentity(test_backend.IdentityTests): user = {'id': 'fake1', 'name': 'fake1', 'password': 'fakepass1', + 'domain_id': CONF.identity.default_domain_id, 'tenants': ['bar']} self.identity_api.create_user('fake1', user) user_ref = self.identity_api.get_user('fake1') @@ -71,14 +80,16 @@ class BaseLDAPIdentity(test_backend.IdentityTests): 'fake1') def test_configurable_forbidden_user_actions(self): - CONF.ldap.user_allow_create = False - CONF.ldap.user_allow_update = False - CONF.ldap.user_allow_delete = False - self.load_backends() + conf = self.get_config(CONF.identity.default_domain_id) + conf.ldap.user_allow_create = False + conf.ldap.user_allow_update = False + conf.ldap.user_allow_delete = False + self.reload_backends(CONF.identity.default_domain_id) user = {'id': 'fake1', 'name': 'fake1', 'password': 'fakepass1', + 'domain_id': CONF.identity.default_domain_id, 'tenants': ['bar']} self.assertRaises(exception.ForbiddenAction, self.identity_api.create_user, @@ -100,8 +111,9 @@ class BaseLDAPIdentity(test_backend.IdentityTests): self.user_foo.pop('password') self.assertDictEqual(user_ref, self.user_foo) - CONF.ldap.user_filter = '(CN=DOES_NOT_MATCH)' - self.load_backends() + conf = self.get_config(user_ref['domain_id']) + conf.ldap.user_filter = '(CN=DOES_NOT_MATCH)' + self.reload_backends(user_ref['domain_id']) self.assertRaises(exception.UserNotFound, self.identity_api.get_user, self.user_foo['id']) @@ -205,18 +217,21 @@ class BaseLDAPIdentity(test_backend.IdentityTests): # Create a group group_id = None - group = dict(name=uuid.uuid4().hex) + group = dict(name=uuid.uuid4().hex, + domain_id=CONF.identity.default_domain_id) group_id = self.identity_api.create_group(group_id, group)['id'] # Create a couple of users and add them to the group. user_id = None - user = dict(name=uuid.uuid4().hex, id=uuid.uuid4().hex) + user = dict(name=uuid.uuid4().hex, id=uuid.uuid4().hex, + domain_id=CONF.identity.default_domain_id) user_1_id = self.identity_api.create_user(user_id, user)['id'] self.identity_api.add_user_to_group(user_1_id, group_id) user_id = None - user = dict(name=uuid.uuid4().hex, id=uuid.uuid4().hex) + user = dict(name=uuid.uuid4().hex, id=uuid.uuid4().hex, + domain_id=CONF.identity.default_domain_id) user_2_id = self.identity_api.create_user(user_id, user)['id'] self.identity_api.add_user_to_group(user_2_id, group_id) @@ -224,7 +239,9 @@ class BaseLDAPIdentity(test_backend.IdentityTests): # Delete user 2 # NOTE(blk-u): need to go directly to user interface to keep from # updating the group. - self.identity_api.driver.user.delete(user_2_id) + driver = self.identity_api._select_identity_driver( + user['domain_id']) + driver.user.delete(user_2_id) # List group users and verify only user 1. res = self.identity_api.list_users_in_group(group_id) @@ -249,13 +266,16 @@ class BaseLDAPIdentity(test_backend.IdentityTests): self.identity_api.create_user(user['id'], user) self.identity_api.add_user_to_project(self.tenant_baz['id'], user['id']) - self.identity_api.driver.user.LDAP_USER = None - self.identity_api.driver.user.LDAP_PASSWORD = None + driver = self.identity_api._select_identity_driver( + user['domain_id']) + driver.user.LDAP_USER = None + driver.user.LDAP_PASSWORD = None self.assertRaises(AssertionError, self.identity_api.authenticate, user_id=user['id'], - password=None) + password=None, + domain_scope=user['domain_id']) # (spzala)The group and domain crud tests below override the standard ones # in test_backend.py so that we can exclude the update name test, since we @@ -460,7 +480,8 @@ class LDAPIdentity(test.TestCase, BaseLDAPIdentity): self.load_backends() self.load_fixtures(default_fixtures) - user = {'id': 'fake1', 'name': 'fake1', 'enabled': True} + user = {'id': 'fake1', 'name': 'fake1', 'enabled': True, + 'domain_id': CONF.identity.default_domain_id} self.identity_api.create_user('fake1', user) user_ref = self.identity_api.get_user('fake1') self.assertEqual(user_ref['enabled'], True) @@ -512,6 +533,7 @@ class LDAPIdentity(test.TestCase, BaseLDAPIdentity): 'id': 'extra_attributes', 'name': 'EXTRA_ATTRIBUTES', 'password': 'extra', + 'domain_id': CONF.identity.default_domain_id } self.identity_api.create_user(user['id'], user) dn, attrs = self.identity_api.driver.user._ldap_get(user['id']) @@ -745,3 +767,230 @@ class LdapIdentitySqlAssignment(sql.Base, test.TestCase, BaseLDAPIdentity): def test_role_filter(self): self.skipTest( 'N/A: Not part of SQL backend') + + +class MultiLDAPandSQLIdentity(sql.Base, test.TestCase, BaseLDAPIdentity): + """Class to test common SQL plus individual LDAP backends. + + We define a set of domains and domain-specific backends: + + - A separate LDAP backend for the default domain + - A separate LDAP backend for domain1 + - domain2 shares the same LDAP as domain1, but uses a different + tree attach point + - An SQL backend for all other domains (which will include domain3 + and domain4) + + Normally one would expect that the default domain would be handled as + part of the "other domains" - however the above provides better + test coverage since most of the existing backend tests use the default + domain. + + """ + def setUp(self): + super(MultiLDAPandSQLIdentity, self).setUp() + + self._set_config() + self.load_backends() + self.engine = self.get_engine() + sql.ModelBase.metadata.create_all(bind=self.engine) + self._setup_domain_test_data() + + # All initial domain data setup complete, time to switch on support + # for separate backends per domain. + + self.orig_config_domains_enabled = ( + config.CONF.identity.domain_specific_drivers_enabled) + self.opt_in_group('identity', domain_specific_drivers_enabled=True) + self.orig_config_dir = ( + config.CONF.identity.domain_config_dir) + self.opt_in_group('identity', domain_config_dir=test.TESTSDIR) + self._set_domain_configs() + self.clear_database() + self.load_fixtures(default_fixtures) + + def tearDown(self): + super(MultiLDAPandSQLIdentity, self).tearDown() + self.opt_in_group( + 'identity', + domain_config_dir=self.orig_config_dir) + self.opt_in_group( + 'identity', + domain_specific_drivers_enabled=self.orig_config_domains_enabled) + sql.ModelBase.metadata.drop_all(bind=self.engine) + self.engine.dispose() + sql.set_global_engine(None) + + def _set_config(self): + self.config([test.etcdir('keystone.conf.sample'), + test.testsdir('test_overrides.conf'), + test.testsdir('backend_multi_ldap_sql.conf')]) + + def _setup_domain_test_data(self): + + def create_domain(domain): + try: + ref = self.assignment_api.create_domain( + domain['id'], domain) + except exception.Conflict: + ref = ( + self.assignment_api.get_domain_by_name(domain['name'])) + return ref + + self.domain_default = create_domain(assignment.DEFAULT_DOMAIN) + self.domain1 = create_domain( + {'id': uuid.uuid4().hex, 'name': 'domain1'}) + self.domain2 = create_domain( + {'id': uuid.uuid4().hex, 'name': 'domain2'}) + self.domain3 = create_domain( + {'id': uuid.uuid4().hex, 'name': 'domain3'}) + self.domain4 = create_domain( + {'id': uuid.uuid4().hex, 'name': 'domain4'}) + + def _set_domain_configs(self): + # We need to load the domain configs explicitly to ensure the + # test overrides are included. + self.identity_api.domain_configs._load_config( + self.identity_api.assignment_api, + [test.etcdir('keystone.conf.sample'), + test.testsdir('test_overrides.conf'), + test.testsdir('backend_multi_ldap_sql.conf'), + test.testsdir('keystone.Default.conf')], + 'Default') + self.identity_api.domain_configs._load_config( + self.identity_api.assignment_api, + [test.etcdir('keystone.conf.sample'), + test.testsdir('test_overrides.conf'), + test.testsdir('backend_multi_ldap_sql.conf'), + test.testsdir('keystone.domain1.conf')], + 'domain1') + self.identity_api.domain_configs._load_config( + self.identity_api.assignment_api, + [test.etcdir('keystone.conf.sample'), + test.testsdir('test_overrides.conf'), + test.testsdir('backend_multi_ldap_sql.conf'), + test.testsdir('keystone.domain2.conf')], + 'domain2') + + def reload_backends(self, domain_id): + # Just reload the driver for this domain - which will pickup + # any updated cfg + self.identity_api.domain_configs.reload_domain_driver( + self.identity_api.assignment_api, domain_id) + + def get_config(self, domain_id): + # Get the config for this domain, will return CONF + # if no specific config defined for this domain + return self.identity_api.domain_configs.get_domain_conf(domain_id) + + def test_list_domains(self): + self.skipTest( + 'N/A: Not relevant for multi ldap testing') + + def test_domain_segregation(self): + """Test that separate configs have segregated the domain. + + Test Plan: + - Create a user in each of the domains + - Make sure that you can only find a given user in its + relevant domain + - Make sure that for a backend that supports multiple domains + you can get the users via any of the domain scopes + + """ + def create_user(domain_id): + user = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain_id, + 'password': uuid.uuid4().hex, + 'enabled': True} + self.identity_api.create_user(user['id'], user) + return user + + userd = create_user(CONF.identity.default_domain_id) + user1 = create_user(self.domain1['id']) + user2 = create_user(self.domain2['id']) + user3 = create_user(self.domain3['id']) + user4 = create_user(self.domain4['id']) + + # Now check that I can read user1 with the appropriate domain + # scope, but won't find it if the wrong scope is used + + ref = self.identity_api.get_user( + userd['id'], domain_scope=CONF.identity.default_domain_id) + del userd['password'] + self.assertDictEqual(ref, userd) + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user, + userd['id'], + domain_scope=self.domain1['id']) + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user, + userd['id'], + domain_scope=self.domain2['id']) + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user, + userd['id'], + domain_scope=self.domain3['id']) + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user, + userd['id'], + domain_scope=self.domain4['id']) + + ref = self.identity_api.get_user( + user1['id'], domain_scope=self.domain1['id']) + del user1['password'] + self.assertDictEqual(ref, user1) + ref = self.identity_api.get_user( + user2['id'], domain_scope=self.domain2['id']) + del user2['password'] + self.assertDictEqual(ref, user2) + + # Domains 3 and 4 share the same backend, so you should be + # able to see user3 and 4 from either + + ref = self.identity_api.get_user( + user3['id'], domain_scope=self.domain3['id']) + del user3['password'] + self.assertDictEqual(ref, user3) + ref = self.identity_api.get_user( + user4['id'], domain_scope=self.domain4['id']) + del user4['password'] + self.assertDictEqual(ref, user4) + ref = self.identity_api.get_user( + user3['id'], domain_scope=self.domain4['id']) + self.assertDictEqual(ref, user3) + ref = self.identity_api.get_user( + user4['id'], domain_scope=self.domain3['id']) + self.assertDictEqual(ref, user4) + + def test_scanning_of_config_dir(self): + """Test the Manager class scans the config directory. + + The setup for the main tests above load the domain configs directly + so that the test overrides can be included. This test just makes sure + that the standard config directory scanning does pick up the relevant + domain config files. + + """ + # Confirm that config has drivers_enabled as True, which we will + # check has been set to False later in this test + self.assertTrue(config.CONF.identity.domain_specific_drivers_enabled) + self.load_backends() + # Execute any command to trigger the lazy loading of domain configs + self.identity_api.list_users(domain_scope=self.domain1['id']) + # ...and now check the domain configs have been set up + self.assertIn('default', self.identity_api.domain_configs) + self.assertIn(self.domain1['id'], self.identity_api.domain_configs) + self.assertIn(self.domain2['id'], self.identity_api.domain_configs) + self.assertNotIn(self.domain3['id'], self.identity_api.domain_configs) + self.assertNotIn(self.domain4['id'], self.identity_api.domain_configs) + + # Finally check that a domain specific config contains items from both + # the primary config and the domain specific config + conf = self.identity_api.domain_configs.get_domain_conf( + self.domain1['id']) + # This should now be false, as is the default, since this is not + # set in the standard primary config file + self.assertFalse(conf.identity.domain_specific_drivers_enabled) + # ..and make sure a domain-specifc options is also set + self.assertEqual(conf.ldap.url, 'fake://memory1') diff --git a/keystone/token/backends/memcache.py b/keystone/token/backends/memcache.py index a07a516b..d0d59eef 100644 --- a/keystone/token/backends/memcache.py +++ b/keystone/token/backends/memcache.py @@ -29,8 +29,6 @@ from keystone import token CONF = config.CONF -config.register_str('servers', group='memcache', default='localhost:11211') -config.register_int('max_compare_and_set_retry', group='memcache', default=16) LOG = logging.getLogger(__name__) diff --git a/keystone/token/core.py b/keystone/token/core.py index 3959586b..e8d04a7e 100644 --- a/keystone/token/core.py +++ b/keystone/token/core.py @@ -29,7 +29,7 @@ from keystone.openstack.common import timeutils CONF = config.CONF -config.register_int('expiration', group='token', default=86400) + LOG = logging.getLogger(__name__) |