Skip to content

Commit

Permalink
Trusts
Browse files Browse the repository at this point in the history
Blueprint trusts

creates a trust.  Using a trust, one user (the trustee), can then
create tokens with a subset of another user's (the trustor) roles and
projects.
If the impersonate flag in the trust is set, the token user_id is set
to the trustor's user ID
If the impersonate flag is not set, the token's user_is is set to the
trustee's user ID

check that both trustor and trustee are enabled prior to creating
the trust token.

sql and kvs backends
sql upgrade scripts
unit tests for backends, auth and v3 api
modifications to the trust controller for creating tokens
Authenticates that only user can be trustor in create
Deleting a trust invalidates all tokens created from that trust
Adds the trust id and the id of the trustee to the header of the token
policy rules for trust

This version has a workaround for testing against the KVS version
of the Service catalog

Change-Id: I5745f4d9a4180b59671a143a55ed87019e98ec76
  • Loading branch information
Adam Young authored and openstack-gerrit committed Mar 5, 2013
1 parent ab6e552 commit 601eeb5
Show file tree
Hide file tree
Showing 36 changed files with 1,645 additions and 99 deletions.
20 changes: 17 additions & 3 deletions etc/policy.json
@@ -1,5 +1,9 @@
{
"admin_required": [["role:admin"], ["is_admin:1"]],
"owner" : [["user_id:%(user_id)s"]],
"admin_or_owner": [["rule:admin_required"], ["rule:owner"]],

"default": [["rule:admin_required"]],

"identity:get_service": [["rule:admin_required"]],
"identity:list_services": [["rule:admin_required"]],
Expand All @@ -21,8 +25,9 @@

"identity:get_project": [["rule:admin_required"]],
"identity:list_projects": [["rule:admin_required"]],
"identity:list_user_projects": [["rule:admin_required"], ["user_id:%(user_id)s"]],
"identity:create_project": [["rule:admin_required"]],
"identity:list_user_projects": [["rule:admin_required"],
["user_id:%(user_id)s"]],
"identity:create_project": [["rule:admin_or_owner"]],
"identity:update_project": [["rule:admin_required"]],
"identity:delete_project": [["rule:admin_required"]],

Expand Down Expand Up @@ -68,5 +73,14 @@
"identity:check_token": [["rule:admin_required"]],
"identity:validate_token": [["rule:admin_required"]],
"identity:revocation_list": [["rule:admin_required"]],
"identity:revoke_token": [["rule:admin_required"], ["user_id:%(user_id)s"]]
"identity:revoke_token": [["rule:admin_required"],
["user_id:%(user_id)s"]],

"identity:create_trust": [["user_id:%(trust.trustor_user_id)s"]],
"identity:get_trust": [["rule:admin_or_owner"]],
"identity:list_trusts": [["@"]],
"identity:list_roles_for_trust": [["@"]],
"identity:check_role_for_trust": [["@"]],
"identity:get_role_for_trust": [["@"]],
"identity:delete_trust": [["@"]]
}
85 changes: 57 additions & 28 deletions keystone/auth/controllers.py
Expand Up @@ -24,6 +24,7 @@
from keystone import exception
from keystone import identity
from keystone import token
from keystone import trust
from keystone.openstack.common import importutils


Expand Down Expand Up @@ -63,13 +64,15 @@ class AuthInfo(object):

def __init__(self, context, auth=None):
self.identity_api = identity.Manager()
self.trust_api = trust.Manager()
self.context = context
self.auth = auth
self._scope_data = (None, None)
# self._scope_data is (domain_id, project_id)
# project scope: (None, project_id)
# domain scope: (domain_id, None)
# unscoped: (None, None)
self._scope_data = (None, None, None)
# self._scope_data is (domain_id, project_id, trust_ref)
# project scope: (None, project_id, None)
# domain scope: (domain_id, None, None)
# trust scope: (None, None, trust_id)
# unscoped: (None, None, None)
self._validate_and_normalize_auth_data()

def _assert_project_is_enabled(self, project_ref):
Expand Down Expand Up @@ -136,6 +139,16 @@ def _lookup_project(self, project_info):
self._assert_project_is_enabled(project_ref)
return project_ref

def _lookup_trust(self, trust_info):
trust_id = trust_info.get('id')
if not trust_id:
raise exception.ValidationError(attribute='trust_id',
target='trust')
trust = self.trust_api.get_trust(self.context, trust_id)
if not trust:
raise exception.TrustNotFound(trust_id)
return trust

def lookup_user(self, user_info):
user_id = user_info.get('id')
user_name = user_info.get('name')
Expand Down Expand Up @@ -165,25 +178,28 @@ def _validate_and_normalize_scope_data(self):
""" Validate and normalize scope data """
if 'scope' not in self.auth:
return

# if scoped, only to a project or domain, but not both
if ('project' not in self.auth['scope'] and
'domain' not in self.auth['scope']):
# neither domain or project provided
raise exception.ValidationError(attribute='project or domain',
target='scope')
if ('project' in self.auth['scope'] and
'domain' in self.auth['scope']):
# both domain and project provided
raise exception.ValidationError(attribute='project or domain',
target='scope')
if sum(['project' in self.auth['scope'],
'domain' in self.auth['scope'],
'trust' in self.auth['scope']]) != 1:
raise exception.ValidationError(
attribute='project, domain, or trust',
target='scope')

if 'project' in self.auth['scope']:
project_ref = self._lookup_project(self.auth['scope']['project'])
self._scope_data = (None, project_ref['id'])
else:
self._scope_data = (None, project_ref['id'], None)
elif 'domain' in self.auth['scope']:
domain_ref = self._lookup_domain(self.auth['scope']['domain'])
self._scope_data = (domain_ref['id'], None)
self._scope_data = (domain_ref['id'], None, None)
elif 'trust' in self.auth['scope']:
trust_ref = self._lookup_trust(self.auth['scope']['trust'])
#TODO ayoung when trusts support domain, Fill in domain data here
if 'project_id' in trust_ref:
project_ref = self._lookup_project(
{'id': trust_ref['project_id']})
self._scope_data = (None, project_ref['id'], trust_ref)
else:
self._scope_data = (None, None, trust_ref)

def _validate_auth_methods(self):
# make sure auth methods are provided
Expand Down Expand Up @@ -236,20 +252,31 @@ def get_scope(self):
Verify and return the scoping information.
:returns: (domain_id, project_id). If scope to a project,
(None, project_id) will be returned. If scope to a domain,
(domain_id, None) will be returned. If unscope,
(None, None) will be returned.
:returns: (domain_id, project_id, trust_ref).
If scope to a project, (None, project_id, None)
will be returned.
If scoped to a domain, (domain_id, None,None)
will be returned.
If scoped to a trust, (None, project_id, trust_ref),
Will be returned, where the project_id comes from the
trust definition.
If unscoped, (None, None, None) will be returned.
"""
return self._scope_data

def set_scope(self, domain_id=None, project_id=None):
def set_scope(self, domain_id=None, project_id=None, trust=None):
""" Set scope information. """
if domain_id and project_id:
msg = _('Scoping to both domain and project is not allowed')
raise ValueError(msg)
self._scope_data = (domain_id, project_id)
if domain_id and trust:
msg = _('Scoping to both domain and trust is not allowed')
raise ValueError(msg)
if project_id and trust:
msg = _('Scoping to both project and trust is not allowed')
raise ValueError(msg)
self._scope_data = (domain_id, project_id, trust)


class Auth(controller.V3Controller):
Expand Down Expand Up @@ -278,8 +305,10 @@ def authenticate_for_token(self, context, auth=None):
raise exception.Unauthorized(e)

def _check_and_set_default_scoping(self, context, auth_info, auth_context):
(domain_id, project_id) = auth_info.get_scope()
if domain_id or project_id:
(domain_id, project_id, trust) = auth_info.get_scope()
if trust:
project_id = trust['project_id']
if domain_id or project_id or trust:
# scope is specified
return

Expand Down
2 changes: 2 additions & 0 deletions keystone/auth/methods/token.py
Expand Up @@ -48,6 +48,8 @@ def authenticate(self, context, auth_payload, user_context):
token_ref['token_data']['token']['extras'])
user_context['method_names'].extend(
token_ref['token_data']['token']['methods'])
if 'trust' in token_ref['token_data']:
raise exception.Forbidden(e)
except AssertionError as e:
LOG.error(e)
raise exception.Unauthorized(e)
91 changes: 72 additions & 19 deletions keystone/auth/token_factory.py
Expand Up @@ -28,6 +28,7 @@
from keystone import exception
from keystone import identity
from keystone import token as token_module
from keystone import trust
from keystone.openstack.common import jsonutils
from keystone.openstack.common import timeutils

Expand All @@ -42,6 +43,7 @@ class TokenDataHelper(object):
def __init__(self, context):
self.identity_api = identity.Manager()
self.catalog_api = catalog.Manager()
self.trust_api = trust.Manager()
self.context = context

def _get_filtered_domain(self, domain_id):
Expand Down Expand Up @@ -100,33 +102,77 @@ def _get_roles_for_user(self, user_id, domain_id, project_id):
roles = self._get_project_roles_for_user(user_id, project_id)
return roles

def _populate_user(self, token_data, user_id, domain_id, project_id):
def _populate_user(self, token_data, user_id, domain_id, project_id,
trust):
user_ref = self.identity_api.get_user(self.context,
user_id)
if trust:
trustor_user_ref = (self.identity_api.get_user(self.context,
trust['trustor_user_id']))
if not trustor_user_ref['enabled']:
raise exception.Forbidden()
if trust['impersonation']:
user_ref = trustor_user_ref
token_data['trust'] = (
{
'id': trust['id'],
'trustor_user': {'id': trust['trustor_user_id']},
'trustee_user': {'id': trust['trustee_user_id']},
'impersonation': trust['impersonation']
})
filtered_user = {
'id': user_ref['id'],
'name': user_ref['name'],
'domain': self._get_filtered_domain(user_ref['domain_id'])}
token_data['user'] = filtered_user

def _populate_roles(self, token_data, user_id, domain_id, project_id):
if domain_id or project_id:
roles = self._get_roles_for_user(user_id, domain_id, project_id)
# we only care about id and name
def _populate_roles(self, token_data, user_id, domain_id, project_id,
trust):
if trust:
token_user_id = trust['trustor_user_id']
token_project_id = trust['project_id']
#trusts do not support domains yet
token_domain_id = None
else:
token_user_id = user_id
token_project_id = project_id
token_domain_id = domain_id

if token_domain_id or token_project_id:
roles = self._get_roles_for_user(token_user_id,
token_domain_id,
token_project_id)
filtered_roles = []
for role in roles:
filtered_roles.append({'id': role['id'], 'name': role['name']})
if trust:
for trust_role in trust['roles']:
match_roles = [x for x in roles
if x['id'] == trust_role['id']]
if match_roles:
filtered_roles.append(match_roles[0])
else:
raise exception.Forbidden()
else:
for role in roles:
filtered_roles.append({'id': role['id'],
'name': role['name']})
token_data['roles'] = filtered_roles

def _populate_service_catalog(self, token_data, user_id,
domain_id, project_id):
domain_id, project_id, trust):
if trust:
user_id = trust['trustor_user_id']
if project_id or domain_id:
service_catalog = self.catalog_api.get_v3_catalog(
self.context, user_id, project_id)
try:
service_catalog = self.catalog_api.get_v3_catalog(
self.context, user_id, project_id)
#TODO KVS backend needs a sample implementation
except exception.NotImplemented:
service_catalog = {}
# TODO(gyee): v3 service catalog is not quite completed yet
#TODO Enforce Endpoints for trust
token_data['catalog'] = service_catalog

def _populate_token(self, token_data, expires=None):
def _populate_token(self, token_data, expires=None, trust=None):
if not expires:
expires = token_module.default_expire_time()
if not isinstance(expires, basestring):
Expand All @@ -135,15 +181,20 @@ def _populate_token(self, token_data, expires=None):
token_data['issued_at'] = timeutils.isotime(subsecond=True)

def get_token_data(self, user_id, method_names, extras,
domain_id=None, project_id=None, expires=None):
domain_id=None, project_id=None, expires=None,
trust=None):
token_data = {'methods': method_names,
'extras': extras}
if trust:
if user_id != trust['trustee_user_id']:
raise exception.Forbidden()

self._populate_scope(token_data, domain_id, project_id)
self._populate_user(token_data, user_id, domain_id, project_id)
self._populate_roles(token_data, user_id, domain_id, project_id)
self._populate_user(token_data, user_id, domain_id, project_id, trust)
self._populate_roles(token_data, user_id, domain_id, project_id, trust)
self._populate_service_catalog(token_data, user_id, domain_id,
project_id)
self._populate_token(token_data, expires)
project_id, trust)
self._populate_token(token_data, expires, trust)
return {'token': token_data}


Expand Down Expand Up @@ -189,7 +240,7 @@ def recreate_token_data(context, token_data=None, expires=None,

def create_token(context, auth_context, auth_info):
token_data_helper = TokenDataHelper(context)
(domain_id, project_id) = auth_info.get_scope()
(domain_id, project_id, trust) = auth_info.get_scope()
method_names = list(set(auth_info.get_method_names() +
auth_context.get('method_names', [])))
token_data = token_data_helper.get_token_data(
Expand All @@ -198,7 +249,9 @@ def create_token(context, auth_context, auth_info):
auth_context['extras'],
domain_id,
project_id,
auth_context.get('expires_at', None))
auth_context.get('expires_at', None),
trust)

if CONF.signing.token_format == 'UUID':
token_id = uuid.uuid4().hex
elif CONF.signing.token_format == 'PKI':
Expand All @@ -214,7 +267,7 @@ def create_token(context, auth_context, auth_info):
try:
expiry = token_data['token']['expires_at']
if isinstance(expiry, basestring):
expiry = timeutils.parse_isotime(expiry)
expiry = timeutils.normalize_time(timeutils.parse_isotime(expiry))
role_ids = []
if 'project' in token_data['token']:
# project-scoped token, fill in the v2 token data
Expand Down
3 changes: 2 additions & 1 deletion keystone/common/controller.py
Expand Up @@ -148,7 +148,8 @@ def wrapper(self, context, **kwargs):
return _filterprotected


@dependency.requires('identity_api', 'policy_api', 'token_api', 'catalog_api')
@dependency.requires('identity_api', 'policy_api', 'token_api',
'trust_api', 'catalog_api')
class V2Controller(wsgi.Application):
"""Base controller class for Identity API v2."""

Expand Down
15 changes: 15 additions & 0 deletions keystone/common/models.py
Expand Up @@ -42,6 +42,7 @@ class Token(Model):
user
tenant
metadata
trust_id
"""

required_keys = ('id', 'expires')
Expand Down Expand Up @@ -147,3 +148,17 @@ class Role(Model):

required_keys = ('id', 'name')
optional_keys = tuple()


class Trust(Model):
"""Trust object.
Required keys:
id
trustor_user_id
trustee_user_id
project_id
"""

required_keys = ('id', 'trustor_user_id', 'trustee_user_id', 'project_id')
optional_keys = tuple('expires_at')

0 comments on commit 601eeb5

Please sign in to comment.