5de1770931
Cleanup code that references users, projects or domains without necessary scoping or filtering throughout the charm. Add logging of domain name in contexts where this is relevant. Tighten rule:service_role to require role:service and token scoped to project config('service-tenant') created in SERVICE_DOMAIN. This ensures that if you have a deployment with end-user access to assign roles within their own domains they will not gain privileged access simply by assigning the service role to one of their own users. Allow users authorized by rule:service_role to perform identity:list_projects. This is required to allow Ceilometer to operate without Admin privileges. Services are given a user in project config('service-tenant') in SERVICE_DOMAIN for v3 authentication / authorization. As of Mitaka Keystone v3 policy the 'service' role is sufficient for services to validate tokens. Services are also given a user in project config('service-tenant') in DEFAULT_DOMAIN to support services still configured with v2.0 authentication / authorization. This will allow us to transition from v2.0 based authentication / authorization and existing services and charms will continue to operate as before. This will also allow the end-user to roll their deployment up to api_version 3 and back to api_version 2 as needed. Services and charms that has made the transition to fully use the v3 API for authentication and authorization will gain full access to domains and projects across the deployment. The first charm to make use of this is charm-ceilometer. Closes-Bug: 1636098 Change-Id: If1518029c43476a5e14bf94596197eabe663499c
285 lines
11 KiB
Python
285 lines
11 KiB
Python
#!/usr/bin/python
|
|
#
|
|
# Copyright 2016 Canonical Ltd
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
from keystoneclient.v2_0 import client
|
|
from keystoneclient.v3 import client as keystoneclient_v3
|
|
from keystoneclient.auth import token_endpoint
|
|
from keystoneclient import session, exceptions
|
|
from charmhelpers.core.decorators import retry_on_exception
|
|
|
|
# Early versions of keystoneclient lib do not have an explicit
|
|
# ConnectionRefused
|
|
if hasattr(exceptions, 'ConnectionRefused'):
|
|
econnrefused = exceptions.ConnectionRefused
|
|
else:
|
|
econnrefused = exceptions.ConnectionError
|
|
|
|
|
|
def _get_keystone_manager_class(endpoint, token, api_version):
|
|
"""Return KeystoneManager class for the given API version
|
|
@param endpoint: the keystone endpoint to point client at
|
|
@param token: the keystone admin_token
|
|
@param api_version: version of the keystone api the client should use
|
|
@returns keystonemanager class used for interrogating keystone
|
|
"""
|
|
if api_version == 2:
|
|
return KeystoneManager2(endpoint, token)
|
|
if api_version == 3:
|
|
return KeystoneManager3(endpoint, token)
|
|
raise ValueError('No manager found for api version {}'.format(api_version))
|
|
|
|
|
|
@retry_on_exception(5, base_delay=3, exc_type=econnrefused)
|
|
def get_keystone_manager(endpoint, token, api_version=None):
|
|
"""Return a keystonemanager for the correct API version
|
|
|
|
If api_version has not been set then create a manager based on the endpoint
|
|
Use this manager to query the catalogue and determine which api version
|
|
should actually be being used. Return the correct client based on that.
|
|
Function is wrapped in a retry_on_exception to catch the case where the
|
|
keystone service is still initialising and not responding to requests yet.
|
|
XXX I think the keystone client should be able to do version
|
|
detection automatically so the code below could be greatly
|
|
simplified
|
|
|
|
@param endpoint: the keystone endpoint to point client at
|
|
@param token: the keystone admin_token
|
|
@param api_version: version of the keystone api the client should use
|
|
@returns keystonemanager class used for interrogating keystone
|
|
"""
|
|
if api_version:
|
|
return _get_keystone_manager_class(endpoint, token, api_version)
|
|
else:
|
|
if 'v2.0' in endpoint.split('/'):
|
|
manager = _get_keystone_manager_class(endpoint, token, 2)
|
|
else:
|
|
manager = _get_keystone_manager_class(endpoint, token, 3)
|
|
if endpoint.endswith('/'):
|
|
base_ep = endpoint.rsplit('/', 2)[0]
|
|
else:
|
|
base_ep = endpoint.rsplit('/', 1)[0]
|
|
svc_id = None
|
|
for svc in manager.api.services.list():
|
|
if svc.type == 'identity':
|
|
svc_id = svc.id
|
|
version = None
|
|
for ep in manager.api.endpoints.list():
|
|
if ep.service_id == svc_id and hasattr(ep, 'adminurl'):
|
|
version = ep.adminurl.split('/')[-1]
|
|
if version and version == 'v2.0':
|
|
new_ep = base_ep + "/" + 'v2.0'
|
|
return _get_keystone_manager_class(new_ep, token, 2)
|
|
elif version and version == 'v3':
|
|
new_ep = base_ep + "/" + 'v3'
|
|
return _get_keystone_manager_class(new_ep, token, 3)
|
|
else:
|
|
return manager
|
|
|
|
|
|
class KeystoneManager(object):
|
|
|
|
def resolve_domain_id(self, name):
|
|
pass
|
|
|
|
def resolve_role_id(self, name):
|
|
"""Find the role_id of a given role"""
|
|
roles = [r._info for r in self.api.roles.list()]
|
|
for r in roles:
|
|
if name.lower() == r['name'].lower():
|
|
return r['id']
|
|
|
|
def resolve_service_id(self, name, service_type=None):
|
|
"""Find the service_id of a given service"""
|
|
services = [s._info for s in self.api.services.list()]
|
|
for s in services:
|
|
if service_type:
|
|
if (name.lower() == s['name'].lower() and
|
|
service_type == s['type']):
|
|
return s['id']
|
|
else:
|
|
if name.lower() == s['name'].lower():
|
|
return s['id']
|
|
|
|
def resolve_service_id_by_type(self, type):
|
|
"""Find the service_id of a given service"""
|
|
services = [s._info for s in self.api.services.list()]
|
|
for s in services:
|
|
if type == s['type']:
|
|
return s['id']
|
|
|
|
|
|
class KeystoneManager2(KeystoneManager):
|
|
|
|
def __init__(self, endpoint, token):
|
|
self.api_version = 2
|
|
self.api = client.Client(endpoint=endpoint, token=token)
|
|
|
|
def resolve_user_id(self, name, user_domain=None):
|
|
"""Find the user_id of a given user"""
|
|
users = [u._info for u in self.api.users.list()]
|
|
for u in users:
|
|
if name.lower() == u['name'].lower():
|
|
return u['id']
|
|
|
|
def create_endpoints(self, region, service_id, publicurl, adminurl,
|
|
internalurl):
|
|
self.api.endpoints.create(region=region, service_id=service_id,
|
|
publicurl=publicurl, adminurl=adminurl,
|
|
internalurl=internalurl)
|
|
|
|
def tenants_list(self):
|
|
return self.api.tenants.list()
|
|
|
|
def resolve_tenant_id(self, name, domain=None):
|
|
"""Find the tenant_id of a given tenant"""
|
|
tenants = [t._info for t in self.api.tenants.list()]
|
|
for t in tenants:
|
|
if name.lower() == t['name'].lower():
|
|
return t['id']
|
|
|
|
def create_tenant(self, tenant_name, description, domain='default'):
|
|
self.api.tenants.create(tenant_name=tenant_name,
|
|
description=description)
|
|
|
|
def delete_tenant(self, tenant_id):
|
|
self.api.tenants.delete(tenant_id)
|
|
|
|
def create_user(self, name, password, email, tenant_id=None,
|
|
domain_id=None):
|
|
self.api.users.create(name=name,
|
|
password=password,
|
|
email=email,
|
|
tenant_id=tenant_id)
|
|
|
|
def update_password(self, user, password):
|
|
self.api.users.update_password(user=user, password=password)
|
|
|
|
def roles_for_user(self, user_id, tenant_id=None, domain_id=None):
|
|
return self.api.roles.roles_for_user(user_id, tenant_id)
|
|
|
|
def add_user_role(self, user, role, tenant, domain):
|
|
self.api.roles.add_user_role(user=user, role=role, tenant=tenant)
|
|
|
|
|
|
class KeystoneManager3(KeystoneManager):
|
|
|
|
def __init__(self, endpoint, token):
|
|
self.api_version = 3
|
|
keystone_auth_v3 = token_endpoint.Token(endpoint=endpoint, token=token)
|
|
keystone_session_v3 = session.Session(auth=keystone_auth_v3)
|
|
self.api = keystoneclient_v3.Client(session=keystone_session_v3)
|
|
|
|
def resolve_tenant_id(self, name, domain=None):
|
|
"""Find the tenant_id of a given tenant"""
|
|
if domain:
|
|
domain_id = self.resolve_domain_id(domain)
|
|
tenants = [t._info for t in self.api.projects.list()]
|
|
for t in tenants:
|
|
if name.lower() == t['name'].lower() and \
|
|
(domain is None or t['domain_id'] == domain_id):
|
|
return t['id']
|
|
|
|
def resolve_domain_id(self, name):
|
|
"""Find the domain_id of a given domain"""
|
|
domains = [d._info for d in self.api.domains.list()]
|
|
for d in domains:
|
|
if name.lower() == d['name'].lower():
|
|
return d['id']
|
|
|
|
def resolve_user_id(self, name, user_domain=None):
|
|
"""Find the user_id of a given user"""
|
|
if user_domain:
|
|
domain_id = self.resolve_domain_id(user_domain)
|
|
for user in self.api.users.list():
|
|
if name.lower() == user.name.lower():
|
|
if user_domain:
|
|
if domain_id == user.domain_id:
|
|
return user.id
|
|
else:
|
|
return user.id
|
|
|
|
def create_endpoints(self, region, service_id, publicurl, adminurl,
|
|
internalurl):
|
|
self.api.endpoints.create(service_id, publicurl, interface='public',
|
|
region=region)
|
|
self.api.endpoints.create(service_id, adminurl, interface='admin',
|
|
region=region)
|
|
self.api.endpoints.create(service_id, internalurl,
|
|
interface='internal', region=region)
|
|
|
|
def tenants_list(self):
|
|
return self.api.projects.list()
|
|
|
|
def create_domain(self, domain_name, description):
|
|
self.api.domains.create(domain_name, description=description)
|
|
|
|
def create_tenant(self, tenant_name, description, domain='default'):
|
|
domain_id = self.resolve_domain_id(domain)
|
|
self.api.projects.create(tenant_name, domain_id,
|
|
description=description)
|
|
|
|
def delete_tenant(self, tenant_id):
|
|
self.api.projects.delete(tenant_id)
|
|
|
|
def create_user(self, name, password, email, tenant_id=None,
|
|
domain_id=None):
|
|
if not domain_id:
|
|
domain_id = self.resolve_domain_id('default')
|
|
if tenant_id:
|
|
self.api.users.create(name,
|
|
domain=domain_id,
|
|
password=password,
|
|
email=email,
|
|
project=tenant_id)
|
|
else:
|
|
self.api.users.create(name,
|
|
domain=domain_id,
|
|
password=password,
|
|
email=email)
|
|
|
|
def update_password(self, user, password):
|
|
self.api.users.update(user, password=password)
|
|
|
|
def roles_for_user(self, user_id, tenant_id=None, domain_id=None):
|
|
# Specify either a domain or project, not both
|
|
if domain_id:
|
|
return self.api.roles.list(user_id, domain=domain_id)
|
|
else:
|
|
return self.api.roles.list(user_id, project=tenant_id)
|
|
|
|
def add_user_role(self, user, role, tenant, domain):
|
|
# Specify either a domain or project, not both
|
|
if domain:
|
|
self.api.roles.grant(role, user=user, domain=domain)
|
|
if tenant:
|
|
self.api.roles.grant(role, user=user, project=tenant)
|
|
|
|
def find_endpoint_v3(self, interface, service_id, region):
|
|
found_eps = []
|
|
for ep in self.api.endpoints.list():
|
|
if ep.service_id == service_id and ep.region == region and \
|
|
ep.interface == interface:
|
|
found_eps.append(ep)
|
|
return found_eps
|
|
|
|
def delete_old_endpoint_v3(self, interface, service_id, region, url):
|
|
eps = self.find_endpoint_v3(interface, service_id, region)
|
|
for ep in eps:
|
|
if getattr(ep, 'url') != url:
|
|
self.api.endpoints.delete(ep.id)
|
|
return True
|
|
return False
|