diff --git a/config.yaml b/config.yaml index bacde764..b053d689 100644 --- a/config.yaml +++ b/config.yaml @@ -78,10 +78,10 @@ options: default: 'Admin' type: string description: 'Admin role to be associated with admin and service users' - token-expiry: - default: "2017-02-05T00:00" - type: string - description: "Expiration date of generated admin tokens" + token-expiration: + default: 3600 + type: int + description: "Amount of time a token should remain valid (in seconds)." service-tenant: default: "services" type: string diff --git a/hooks/keystone_context.py b/hooks/keystone_context.py index dbbc237b..0c023688 100644 --- a/hooks/keystone_context.py +++ b/hooks/keystone_context.py @@ -202,6 +202,7 @@ class KeystoneContext(context.OSContextGenerator): ctxt['debug'] = debug and bool_from_string(debug) verbose = config('verbose') ctxt['verbose'] = verbose and bool_from_string(verbose) + ctxt['token_expiration'] = config('token-expiration') ctxt['identity_backend'] = config('identity-backend') ctxt['assignment_backend'] = config('assignment-backend') diff --git a/hooks/keystone_utils.py b/hooks/keystone_utils.py index cf25bfe0..cc1880be 100644 --- a/hooks/keystone_utils.py +++ b/hooks/keystone_utils.py @@ -472,12 +472,14 @@ def create_service_entry(service_name, service_type, service_desc, owner=None): token=get_admin_token()) for service in [s._info for s in manager.api.services.list()]: if service['name'] == service_name: - log("Service entry for '%s' already exists." % service_name) + log("Service entry for '%s' already exists." % service_name, + level=DEBUG) return + manager.api.services.create(name=service_name, service_type=service_type, description=service_desc) - log("Created new service entry '%s'" % service_name) + log("Created new service entry '%s'" % service_name, level=DEBUG) def create_endpoint_template(region, service, publicurl, adminurl, @@ -510,7 +512,8 @@ def create_endpoint_template(region, service, publicurl, adminurl, publicurl=publicurl, adminurl=adminurl, internalurl=internalurl) - log("Created new endpoint template for '%s' in '%s'" % (region, service)) + log("Created new endpoint template for '%s' in '%s'" % (region, service), + level=DEBUG) def create_tenant(name): @@ -522,9 +525,21 @@ def create_tenant(name): if not tenants or name not in [t['name'] for t in tenants]: manager.api.tenants.create(tenant_name=name, description='Created by Juju') - log("Created new tenant: %s" % name) + log("Created new tenant: %s" % name, level=DEBUG) return - log("Tenant '%s' already exists." % name) + + log("Tenant '%s' already exists." % name, level=DEBUG) + + +def user_exists(name): + import manager + manager = manager.KeystoneManager(endpoint=get_local_endpoint(), + token=get_admin_token()) + users = [u._info for u in manager.api.users.list()] + if not users or name not in [u['name'] for u in users]: + return False + + return True def create_user(name, password, tenant): @@ -532,18 +547,20 @@ def create_user(name, password, tenant): import manager manager = manager.KeystoneManager(endpoint=get_local_endpoint(), token=get_admin_token()) - users = [u._info for u in manager.api.users.list()] - if not users or name not in [u['name'] for u in users]: - tenant_id = manager.resolve_tenant_id(tenant) - if not tenant_id: - error_out('Could not resolve tenant_id for tenant %s' % tenant) - manager.api.users.create(name=name, - password=password, - email='juju@localhost', - tenant_id=tenant_id) - log("Created new user '%s' tenant: %s" % (name, tenant_id)) + if user_exists(name): + log("A user named '%s' already exists" % name, level=DEBUG) return - log("A user named '%s' already exists" % name) + + tenant_id = manager.resolve_tenant_id(tenant) + if not tenant_id: + error_out('Could not resolve tenant_id for tenant %s' % tenant) + + manager.api.users.create(name=name, + password=password, + email='juju@localhost', + tenant_id=tenant_id) + log("Created new user '%s' tenant: %s" % (name, tenant_id), + level=DEBUG) def create_role(name, user=None, tenant=None): @@ -554,9 +571,9 @@ def create_role(name, user=None, tenant=None): roles = [r._info for r in manager.api.roles.list()] if not roles or name not in [r['name'] for r in roles]: manager.api.roles.create(name=name) - log("Created new role '%s'" % name) + log("Created new role '%s'" % name, level=DEBUG) else: - log("A role named '%s' already exists" % name) + log("A role named '%s' already exists" % name, level=DEBUG) if not user and not tenant: return @@ -590,10 +607,10 @@ def grant_role(user, role, tenant): role=role_id, tenant=tenant_id) log("Granted user '%s' role '%s' on tenant '%s'" % - (user, role, tenant)) + (user, role, tenant), level=DEBUG) else: log("User '%s' already has role '%s' on tenant '%s'" % - (user, role, tenant)) + (user, role, tenant), level=DEBUG) def store_admin_passwd(passwd): @@ -663,10 +680,9 @@ def ensure_initial_admin(config): 'ldap' and config('ldap-readonly')): passwd = get_admin_passwd() if passwd: - create_user(config('admin-user'), passwd, tenant='admin') - update_user_password(config('admin-user'), passwd) - create_role(config('admin-role'), config('admin-user'), - 'admin') + create_user_credentials(config('admin-user'), 'admin', passwd, + new_roles=[config('admin-role')]) + create_service_entry("keystone", "identity", "Keystone Identity Service") @@ -1260,6 +1276,50 @@ def relation_list(rid): return result +def create_user_credentials(user, tenant, passwd, new_roles=None, grants=None): + """Create user credentials. + + Optionally adds role grants to user and/or creates new roles. + """ + log("Creating service credentials for '%s'" % user, level=DEBUG) + if user_exists(user): + log("User '%s' already exists - updating password" % (user), + level=DEBUG) + update_user_password(user, passwd) + else: + create_user(user, passwd, tenant) + + if grants: + for role in grants: + grant_role(user, role, tenant) + else: + log("No role grants requested for user '%s'" % (user), level=DEBUG) + + if new_roles: + # Allow the remote service to request creation of any additional roles. + # Currently used by Swift and Ceilometer. + for role in new_roles: + log("Creating requested role '%s'" % role, level=DEBUG) + create_role(role, user, tenant) + + return passwd + + +def create_service_credentials(user, new_roles=None): + """Create credentials for service with given username. + + Services are given a user under config('service-tenant') and are given the + config('admin-role') role. Tenant is assumed to already exist, + """ + tenant = config('service-tenant') + if not tenant: + raise Exception("No service tenant provided in config") + + return create_user_credentials(user, tenant, get_service_password(user), + new_roles=new_roles, + grants=[config('admin-role')]) + + def add_service_to_keystone(relation_id=None, remote_unit=None): import manager manager = manager.KeystoneManager(endpoint=get_local_endpoint(), @@ -1390,18 +1450,9 @@ def add_service_to_keystone(relation_id=None, remote_unit=None): return token = get_admin_token() - log("Creating service credentials for '%s'" % service_username) - - service_password = get_service_password(service_username) - create_user(service_username, service_password, config('service-tenant')) - grant_role(service_username, config('admin-role'), - config('service-tenant')) - - # Allow the remote service to request creation of any additional roles. - # Currently used by Swift and Ceilometer. - for role in get_requested_roles(settings): - log("Creating requested role: %s" % role) - create_role(role, service_username, config('service-tenant')) + roles = get_requested_roles(settings) + service_password = create_service_credentials(service_username, + new_roles=roles) # As of https://review.openstack.org/#change,4675, all nodes hosting # an endpoint(s) needs a service username and password assigned to diff --git a/templates/icehouse/keystone.conf b/templates/icehouse/keystone.conf index 5ef7fe35..f92e9262 100644 --- a/templates/icehouse/keystone.conf +++ b/templates/icehouse/keystone.conf @@ -48,8 +48,9 @@ provider = keystone.token.providers.pki.Provider {% elif token_provider == 'pkiz' -%} provider = keystone.token.providers.pkiz.Provider {% else -%} -provider = keystone.token.providers.uuid.Provider -{% endif %} +provider = keystone.token.providers.uuid.Provider +{% endif -%} +expiration = {{ token_expiration }} {% include "parts/section-signing" %} diff --git a/templates/kilo/keystone.conf b/templates/kilo/keystone.conf index 499caa4c..6fd6d8c7 100644 --- a/templates/kilo/keystone.conf +++ b/templates/kilo/keystone.conf @@ -45,7 +45,16 @@ driver = keystone.catalog.backends.sql.Catalog [token] driver = keystone.token.persistence.backends.sql.Token +{% if token_provider == 'pki' -%} +provider = keystone.token.providers.pki.Provider +{% elif token_provider == 'pkiz' -%} +provider = keystone.token.providers.pkiz.Provider +{% else -%} provider = keystone.token.providers.uuid.Provider +{% endif -%} +expiration = {{ token_expiration }} + +{% include "parts/section-signing" %} [cache] diff --git a/unit_tests/test_keystone_utils.py b/unit_tests/test_keystone_utils.py index 4bd5ff62..c617174b 100644 --- a/unit_tests/test_keystone_utils.py +++ b/unit_tests/test_keystone_utils.py @@ -303,6 +303,63 @@ class TestKeystoneUtils(CharmTestCase): adminurl='10.0.0.2', internalurl='192.168.1.2') + @patch.object(utils, 'user_exists') + @patch.object(utils, 'grant_role') + @patch.object(utils, 'create_role') + @patch.object(utils, 'create_user') + def test_create_user_credentials_no_roles(self, mock_create_user, + mock_create_role, + mock_grant_role, + mock_user_exists): + mock_user_exists.return_value = False + utils.create_user_credentials('userA', 'tenantA', 'passA') + mock_create_user.assert_has_calls([call('userA', 'passA', 'tenantA')]) + mock_create_role.assert_has_calls([]) + mock_grant_role.assert_has_calls([]) + + @patch.object(utils, 'user_exists') + @patch.object(utils, 'grant_role') + @patch.object(utils, 'create_role') + @patch.object(utils, 'create_user') + def test_create_user_credentials(self, mock_create_user, mock_create_role, + mock_grant_role, mock_user_exists): + mock_user_exists.return_value = False + utils.create_user_credentials('userA', 'tenantA', 'passA', + grants=['roleA'], new_roles=['roleB']) + mock_create_user.assert_has_calls([call('userA', 'passA', 'tenantA')]) + mock_create_role.assert_has_calls([call('roleB', 'userA', 'tenantA')]) + mock_grant_role.assert_has_calls([call('userA', 'roleA', 'tenantA')]) + + @patch.object(utils, 'update_user_password') + @patch.object(utils, 'user_exists') + @patch.object(utils, 'grant_role') + @patch.object(utils, 'create_role') + @patch.object(utils, 'create_user') + def test_create_user_credentials_user_exists(self, mock_create_user, + mock_create_role, + mock_grant_role, + mock_user_exists, + mock_update_user_password): + mock_user_exists.return_value = True + utils.create_user_credentials('userA', 'tenantA', 'passA', + grants=['roleA'], new_roles=['roleB']) + mock_create_user.assert_has_calls([]) + mock_create_role.assert_has_calls([call('roleB', 'userA', 'tenantA')]) + mock_grant_role.assert_has_calls([call('userA', 'roleA', 'tenantA')]) + mock_update_user_password.assert_has_calls([call('userA', 'passA')]) + + @patch.object(utils, 'get_service_password') + @patch.object(utils, 'create_user_credentials') + def test_create_service_credentials(self, mock_create_user_credentials, + mock_get_service_password): + mock_get_service_password.return_value = 'passA' + cfg = {'service-tenant': 'tenantA', 'admin-role': 'Admin'} + self.config.side_effect = lambda key: cfg.get(key, None) + calls = [call('serviceA', 'tenantA', 'passA', grants=['Admin'], + new_roles=None)] + utils.create_service_credentials('serviceA') + mock_create_user_credentials.assert_has_calls(calls) + def test_ensure_valid_service_incorrect(self): utils.ensure_valid_service('fakeservice') self.log.assert_called_with("Invalid service requested: 'fakeservice'")