diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst index 1c339490e2..2b802c8370 100644 --- a/doc/source/configuration.rst +++ b/doc/source/configuration.rst @@ -96,6 +96,25 @@ order: PasteDeploy configuration file is specified by the ``config_file`` parameter in ``[paste_deploy]`` section of the primary configuration file. If the parameter is not an absolute path, then Keystone looks for it in the same directories as above. If not specified, WSGI pipeline definitions are loaded from the primary configuration file. +Keystone supports the option (disabled by default) to specify identity driver +configurations on a domain by domain basis, allowing, for example, a specific +domain to have its own LDAP or SQL server. This is configured by specifying the +following options:: + + [identity] + domain_specific_drivers_enabled = True + domain_config_dir = /etc/keystone/domains + +Setting ``domain_specific_drivers_enabled`` to True will enable this feature, causing +keystone to look in the ``domain_config_dir`` for config files of the form:: + + keystone..conf + +Options given in the domain specific configuration file will override those in the +primary configuration file for the specified domain only. Domains without a specific +configuration file will continue to use the options from the primary configuration +file. + Authentication Plugins ---------------------- diff --git a/etc/keystone.conf.sample b/etc/keystone.conf.sample index 90efe5f66b..922d90c693 100644 --- a/etc/keystone.conf.sample +++ b/etc/keystone.conf.sample @@ -99,6 +99,14 @@ # There is nothing special about this domain, other than the fact that it must # exist to order to maintain support for your v2 clients. # default_domain_id = default +# +# A subset (or all) of domains can have their own identity driver, each with +# their own partial configuration file in a domain configuration directory. +# Only values specific to the domain need to be placed in the domain specific +# configuration file. This feature is disabled by default; set +# domain_specific_drivers_enabled to True to enable. +# domain_specific_drivers_enabled = False +# domain_config_dir = /etc/keystone/domains # Maximum supported length for user passwords; decrease to improve performance. # max_password_length = 4096 diff --git a/keystone/auth/plugins/password.py b/keystone/auth/plugins/password.py index 66c6d05b1d..b069f4d971 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 7fe73e91ed..db99110bf3 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 5a961d4a3d..61eeac929b 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") + conf.register_opt(cfg.StrOpt(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 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.')) -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) + for section in FILE_OPTIONS: + for option in FILE_OPTIONS[section]: + if section: + conf.register_opt(option, group=section) + else: + conf.register_opt(option) - -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.') - - 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 1bf65cdafc..90818fb458 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 c19e135500..e445887474 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 4abad57a43..27968efc39 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 28f1cf2ce4..c4a43b47fe 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 0323d3d084..bcfb777b87 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 ef3b5d6181..67380f6e6c 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 2a6ee6211d..a545969474 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 65a34a8ad4..84026a58ac 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 67f3beacdf..281e3f1be5 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 7fb630e2c6..7d5882e3c8 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..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 0000000000..59cff76191 --- /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 8d0753359a..b42a870935 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 0000000000..7049afed40 --- /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 0000000000..6b7e2488ad --- /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 0000000000..0ed68eb96d --- /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 52628985cf..8013deec7c 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 6f9cfef9e1..e40e05650e 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 a07a516b06..d0d59eef88 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 3959586b30..e8d04a7eef 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__)