summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2013-03-06 03:43:44 +0000
committerGerrit Code Review <review@openstack.org>2013-03-06 03:43:44 +0000
commitcdeda9416b8ea4af5250fb4bd48865f4d87967b3 (patch)
treeb5177fddbe575c4a74c2e537f4c97ae8df73bdf4
parent6db8b6d480453927ee26a720f2e96c20e13907be (diff)
parent601eeb50b60a2e99041690fe19238202bc203503 (diff)
Merge "Trusts"
-rw-r--r--etc/policy.json20
-rw-r--r--keystone/auth/controllers.py85
-rw-r--r--keystone/auth/methods/token.py2
-rw-r--r--keystone/auth/token_factory.py91
-rw-r--r--keystone/common/controller.py3
-rw-r--r--keystone/common/models.py15
-rw-r--r--keystone/common/sql/migrate_repo/versions/018_add_trust_tables.py68
-rw-r--r--keystone/config.py2
-rw-r--r--keystone/exception.py4
-rw-r--r--keystone/identity/backends/sql.py17
-rw-r--r--keystone/identity/controllers.py35
-rw-r--r--keystone/service.py6
-rw-r--r--keystone/test.py2
-rw-r--r--keystone/token/backends/kvs.py16
-rw-r--r--keystone/token/backends/memcache.py11
-rw-r--r--keystone/token/backends/sql.py20
-rw-r--r--keystone/token/controllers.py71
-rw-r--r--keystone/token/core.py6
-rw-r--r--keystone/trust/__init__.py19
-rw-r--r--keystone/trust/backends/__init__.py0
-rw-r--r--keystone/trust/backends/kvs.py92
-rw-r--r--keystone/trust/backends/sql.py123
-rw-r--r--keystone/trust/controllers.py244
-rw-r--r--keystone/trust/core.py63
-rw-r--r--keystone/trust/routers.py58
-rw-r--r--tests/backend_sql.conf3
-rw-r--r--tests/default_fixtures.py7
-rw-r--r--tests/test_auth.py244
-rw-r--r--tests/test_backend.py88
-rw-r--r--tests/test_backend_kvs.py10
-rw-r--r--tests/test_backend_sql.py8
-rw-r--r--tests/test_content_types.py2
-rw-r--r--tests/test_overrides.conf3
-rw-r--r--tests/test_sql_upgrade.py12
-rw-r--r--tests/test_v3.py8
-rw-r--r--tests/test_v3_trust.py286
36 files changed, 1645 insertions, 99 deletions
diff --git a/etc/policy.json b/etc/policy.json
index a0e77fc2..89365e5e 100644
--- a/etc/policy.json
+++ b/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"]],
@@ -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"]],
@@ -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": [["@"]]
}
diff --git a/keystone/auth/controllers.py b/keystone/auth/controllers.py
index 3760aa3a..96f3dc0c 100644
--- a/keystone/auth/controllers.py
+++ b/keystone/auth/controllers.py
@@ -24,6 +24,7 @@ from keystone import config
from keystone import exception
from keystone import identity
from keystone import token
+from keystone import trust
from keystone.openstack.common import importutils
@@ -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):
@@ -136,6 +139,16 @@ class AuthInfo(object):
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')
@@ -165,25 +178,28 @@ class AuthInfo(object):
""" 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
@@ -236,20 +252,31 @@ class AuthInfo(object):
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):
@@ -278,8 +305,10 @@ class Auth(controller.V3Controller):
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
diff --git a/keystone/auth/methods/token.py b/keystone/auth/methods/token.py
index 10e99cfb..bb7b8d58 100644
--- a/keystone/auth/methods/token.py
+++ b/keystone/auth/methods/token.py
@@ -48,6 +48,8 @@ class Token(auth.AuthMethodHandler):
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)
diff --git a/keystone/auth/token_factory.py b/keystone/auth/token_factory.py
index d6dc68f9..8460aec6 100644
--- a/keystone/auth/token_factory.py
+++ b/keystone/auth/token_factory.py
@@ -28,6 +28,7 @@ from keystone import config
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
@@ -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):
@@ -100,33 +102,77 @@ class TokenDataHelper(object):
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):
@@ -135,15 +181,20 @@ class TokenDataHelper(object):
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}
@@ -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(
@@ -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':
@@ -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
diff --git a/keystone/common/controller.py b/keystone/common/controller.py
index c9300002..f4f3c79d 100644
--- a/keystone/common/controller.py
+++ b/keystone/common/controller.py
@@ -148,7 +148,8 @@ def filterprotected(*filters):
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."""
diff --git a/keystone/common/models.py b/keystone/common/models.py
index f572d382..86a327c2 100644
--- a/keystone/common/models.py
+++ b/keystone/common/models.py
@@ -42,6 +42,7 @@ class Token(Model):
user
tenant
metadata
+ trust_id
"""
required_keys = ('id', 'expires')
@@ -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')
diff --git a/keystone/common/sql/migrate_repo/versions/018_add_trust_tables.py b/keystone/common/sql/migrate_repo/versions/018_add_trust_tables.py
new file mode 100644
index 00000000..77c42ba1
--- /dev/null
+++ b/keystone/common/sql/migrate_repo/versions/018_add_trust_tables.py
@@ -0,0 +1,68 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack LLC
+#
+# 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.
+
+import migrate
+import sqlalchemy as sql
+
+
+def upgrade(migrate_engine):
+ # Upgrade operations go here. Don't create your own engine; bind
+ # migrate_engine to your metadata
+ meta = sql.MetaData()
+ meta.bind = migrate_engine
+
+ user_table = sql.Table('user', meta, autoload=True)
+ role_table = sql.Table('role', meta, autoload=True)
+ tenant_table = sql.Table('project', meta, autoload=True)
+
+ trust_table = sql.Table(
+ 'trust',
+ meta,
+ sql.Column('id', sql.String(64), primary_key=True),
+ sql.Column('trustor_user_id',
+ sql.String(64),
+ unique=False,
+ nullable=False,),
+ sql.Column('trustee_user_id',
+ sql.String(64),
+ unique=False,
+ nullable=False),
+ sql.Column('project_id', sql.String(64),
+ unique=False,
+ nullable=True),
+ sql.Column("impersonation", sql.types.Boolean, nullable=False),
+ sql.Column("deleted_at", sql.types.DateTime, nullable=True),
+ sql.Column("expires_at", sql.types.DateTime, nullable=True),
+ sql.Column('extra', sql.Text()))
+ trust_table.create(migrate_engine, checkfirst=True)
+
+ trust_role_table = sql.Table(
+ 'trust_role',
+ meta,
+ sql.Column('trust_id', sql.String(64), primary_key=True,
+ nullable=False),
+ sql.Column('role_id', sql.String(64), primary_key=True,
+ nullable=False))
+ trust_role_table.create(migrate_engine, checkfirst=True)
+
+
+def downgrade(migrate_engine):
+ meta = sql.MetaData()
+ meta.bind = migrate_engine
+ # Operations to reverse the above upgrade go here.
+ for table_name in ['trust_role', 'trust']:
+ table = sql.Table(table_name, meta, autoload=True)
+ table.drop()
diff --git a/keystone/config.py b/keystone/config.py
index eca692f1..4c319778 100644
--- a/keystone/config.py
+++ b/keystone/config.py
@@ -235,6 +235,8 @@ register_str('driver', group='policy',
default='keystone.policy.backends.sql.Policy')
register_str('driver', group='token',
default='keystone.token.backends.kvs.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',
diff --git a/keystone/exception.py b/keystone/exception.py
index 017db27f..09c43585 100644
--- a/keystone/exception.py
+++ b/keystone/exception.py
@@ -190,6 +190,10 @@ class GroupNotFound(NotFound):
"""Could not find group: %(group_id)s"""
+class TrustNotFound(NotFound):
+ """Could not find trust: %(trust_id)s"""
+
+
class Conflict(Error):
"""Conflict occurred attempting to store %(type)s.
diff --git a/keystone/identity/backends/sql.py b/keystone/identity/backends/sql.py
index e6f07c2e..f36002ce 100644
--- a/keystone/identity/backends/sql.py
+++ b/keystone/identity/backends/sql.py
@@ -14,8 +14,6 @@
# License for the specific language governing permissions and limitations
# under the License.
-import functools
-
from keystone import clean
from keystone import config
from keystone.common import sql
@@ -195,18 +193,7 @@ class Identity(sql.Base, identity.Driver):
# FIXME(gyee): this should really be
# get_roles_for_user_and_project() after the dusts settle
if tenant_id not in self.get_projects_for_user(user_id):
- # get_roles_for_user_and_project() returns a set
- roles = []
- try:
- roles = self.get_roles_for_user_and_project(user_id,
- tenant_id)
- except:
- # FIXME(gyee): we should never get into this situation
- # after user project role migration is completed
- pass
- if not roles:
- raise AssertionError('Invalid tenant')
-
+ raise AssertionError('Invalid project')
try:
tenant_ref = self.get_project(tenant_id)
metadata_ref = self.get_metadata(user_id, tenant_id)
@@ -215,7 +202,6 @@ class Identity(sql.Base, identity.Driver):
metadata_ref = {}
except exception.MetadataNotFound:
metadata_ref = {}
-
return (identity.filter_user(user_ref), tenant_ref, metadata_ref)
def get_project(self, tenant_id):
@@ -622,6 +608,7 @@ class Identity(sql.Base, identity.Driver):
raise exception.DomainNotFound(domain_id=domain_id)
return ref.to_dict()
+ @sql.handle_conflicts(type='domain')
def get_domain_by_name(self, domain_name):
session = self.get_session()
try:
diff --git a/keystone/identity/controllers.py b/keystone/identity/controllers.py
index e5f30e56..e8472987 100644
--- a/keystone/identity/controllers.py
+++ b/keystone/identity/controllers.py
@@ -161,6 +161,24 @@ class Tenant(controller.V2Controller):
return o
+def delete_tokens_for_user(token_api, trust_api, context, user_id, user):
+ try:
+ #First delete tokens that could get other tokens.
+ for token_id in token_api.list_tokens(context, user_id):
+ token_api.delete_token(context, token_id)
+ #now delete trust tokens
+ for trust_id in (trust_api.list_trusts_for_trustee(context, user_id)):
+ token_list = token_api.list_tokens(context, userid,
+ trust_id=trust_id)
+ for token in token_list:
+ token_api.delete_token(context, token)
+ except exception.NotImplemented:
+ # The users status has been changed but tokens remain valid for
+ # backends that can't list tokens for users
+ LOG.warning('User %s status has changed, but existing tokens '
+ 'remain valid' % user_id)
+
+
class User(controller.V2Controller):
def get_user(self, context, user_id):
self.assert_admin(context)
@@ -215,16 +233,12 @@ class User(controller.V2Controller):
self.assert_admin(context)
user_ref = self.identity_api.update_user(context, user_id, user)
- # If the password was changed or the user was disabled we clear tokens
if user.get('password') or not user.get('enabled', True):
- try:
- for token_id in self.token_api.list_tokens(context, user_id):
- self.token_api.delete_token(context, token_id)
- except exception.NotImplemented:
- # The users status has been changed but tokens remain valid for
- # backends that can't list tokens for users
- LOG.warning('User %s status has changed, but existing tokens '
- 'remain valid' % user_id)
+ # If the password was changed or the user was disabled we clear tokens
+ delete_tokens_for_user(self.token_api, self.trust_api,
+ context,
+ user_id,
+ user)
return {'user': self._filter_domain_id(user_ref)}
def delete_user(self, context, user_id):
@@ -329,7 +343,8 @@ class Role(controller.V2Controller):
context, user_id, tenant_id, role_id)
roles = self.identity_api.get_roles_for_user_and_project(
context, user_id, tenant_id)
- self.token_api.revoke_tokens(context, user_id, tenant_id)
+ delete_tokens_for_user(self.token_api, self.trust_api, context,
+ user_id, tenant_id)
# COMPAT(diablo): CRUD extension
def get_role_refs(self, context, user_id):
diff --git a/keystone/service.py b/keystone/service.py
index e4cca53e..423aee06 100644
--- a/keystone/service.py
+++ b/keystone/service.py
@@ -25,6 +25,7 @@ from keystone import identity
from keystone import policy
from keystone import routers
from keystone import token
+from keystone import trust
LOG = logging.getLogger(__name__)
@@ -34,7 +35,8 @@ DRIVERS = dict(
ec2_api=ec2.Manager(),
identity_api=identity.Manager(),
policy_api=policy.Manager(),
- token_api=token.Manager())
+ token_api=token.Manager(),
+ trust_api=trust.Manager())
@logging.fail_gracefully
@@ -81,7 +83,7 @@ def v3_app_factory(global_conf, **local_conf):
conf.update(local_conf)
mapper = routes.Mapper()
v3routers = []
- for module in [auth, catalog, identity, policy]:
+ for module in [auth, catalog, identity, policy, trust]:
module.routers.append_v3_routers(mapper, v3routers)
# TODO(ayoung): put token routes here
return wsgi.ComposingRouter(mapper, v3routers)
diff --git a/keystone/test.py b/keystone/test.py
index 2972314f..7386f552 100644
--- a/keystone/test.py
+++ b/keystone/test.py
@@ -39,6 +39,7 @@ from keystone import exception
from keystone import identity
from keystone import policy
from keystone import token
+from keystone import trust
do_monkeypatch = not os.getenv('STANDARD_THREADS')
@@ -74,6 +75,7 @@ def initialize_drivers():
DRIVERS['identity_api'] = identity.Manager()
DRIVERS['policy_api'] = policy.Manager()
DRIVERS['token_api'] = token.Manager()
+ DRIVERS['trust_api'] = trust.Manager()
return DRIVERS
diff --git a/keystone/token/backends/kvs.py b/keystone/token/backends/kvs.py
index f12fe80d..0adf0579 100644
--- a/keystone/token/backends/kvs.py
+++ b/keystone/token/backends/kvs.py
@@ -31,7 +31,11 @@ class Token(kvs.Base, token.Driver):
ref = self.db.get('token-%s' % token_id)
except exception.NotFound:
raise exception.TokenNotFound(token_id=token_id)
- if ref['expires'] is None or ref['expires'] > timeutils.utcnow():
+ now = timeutils.utcnow()
+ expiry = ref['expires']
+ if expiry is None:
+ raise exception.TokenNotFound(token_id=token_id)
+ if expiry > now:
return copy.deepcopy(ref)
else:
raise exception.TokenNotFound(token_id=token_id)
@@ -39,8 +43,10 @@ class Token(kvs.Base, token.Driver):
def create_token(self, token_id, data):
token_id = token.unique_id(token_id)
data_copy = copy.deepcopy(data)
- if 'expires' not in data:
+ if not data_copy.get('expires'):
data_copy['expires'] = token.default_expire_time()
+ if 'trust_id' in data and data['trust_id'] is None:
+ data_copy.pop('trust_id')
self.db.set('token-%s' % token_id, data_copy)
return copy.deepcopy(data_copy)
@@ -53,7 +59,7 @@ class Token(kvs.Base, token.Driver):
except exception.NotFound:
raise exception.TokenNotFound(token_id=token_id)
- def list_tokens(self, user_id, tenant_id=None):
+ def list_tokens(self, user_id, tenant_id=None, trust_id=None):
tokens = []
now = timeutils.utcnow()
for token, ref in self.db.items():
@@ -72,6 +78,10 @@ class Token(kvs.Base, token.Driver):
continue
if tenant.get('id') != tenant_id:
continue
+ if trust_id is not None:
+ trust = ref.get('trust_id')
+ if not trust:
+ continue
tokens.append(token.split('-', 1)[1])
return tokens
diff --git a/keystone/token/backends/memcache.py b/keystone/token/backends/memcache.py
index efac16fd..b097ab5e 100644
--- a/keystone/token/backends/memcache.py
+++ b/keystone/token/backends/memcache.py
@@ -64,7 +64,7 @@ class Token(token.Driver):
def create_token(self, token_id, data):
data_copy = copy.deepcopy(data)
ptk = self._prefix_token_id(token.unique_id(token_id))
- if 'expires' not in data_copy:
+ if not data_copy.get('expires'):
data_copy['expires'] = token.default_expire_time()
kwargs = {}
if data_copy['expires'] is not None:
@@ -99,7 +99,7 @@ class Token(token.Driver):
self._add_to_revocation_list(data)
return result
- def list_tokens(self, user_id, tenant_id=None):
+ def list_tokens(self, user_id, tenant_id=None, trust_id=None):
tokens = []
user_key = self._prefix_user_id(user_id)
user_record = self.client.get(user_key) or ""
@@ -114,6 +114,13 @@ class Token(token.Driver):
continue
if tenant.get('id') != tenant_id:
continue
+ if trust_id is not None:
+ trust = token_ref.get('trust_id')
+ if not trust:
+ continue
+ if trust != trust_id:
+ continue
+
tokens.append(token_id)
return tokens
diff --git a/keystone/token/backends/sql.py b/keystone/token/backends/sql.py
index 822b869e..62122ebe 100644
--- a/keystone/token/backends/sql.py
+++ b/keystone/token/backends/sql.py
@@ -43,16 +43,18 @@ class Token(sql.Base, token.Driver):
query = query.filter_by(id=token.unique_id(token_id), valid=True)
token_ref = query.first()
now = datetime.datetime.utcnow()
- if token_ref and (not token_ref.expires or now < token_ref.expires):
- return token_ref.to_dict()
- else:
+ if not token_ref:
raise exception.TokenNotFound(token_id=token_id)
+ if not token_ref.expires:
+ raise exception.TokenNotFound(token_id=token_id)
+ if now >= token_ref.expires:
+ raise exception.TokenNotFound(token_id=token_id)
+ return token_ref.to_dict()
def create_token(self, token_id, data):
data_copy = copy.deepcopy(data)
- if 'expires' not in data_copy:
+ if not data_copy.get('expires'):
data_copy['expires'] = token.default_expire_time()
-
token_ref = TokenModel.from_dict(data_copy)
token_ref.id = token.unique_id(token_id)
token_ref.valid = True
@@ -73,7 +75,7 @@ class Token(sql.Base, token.Driver):
token_ref.valid = False
session.flush()
- def list_tokens(self, user_id, tenant_id=None):
+ def list_tokens(self, user_id, tenant_id=None, trust_id=None):
session = self.get_session()
tokens = []
now = timeutils.utcnow()
@@ -93,6 +95,12 @@ class Token(sql.Base, token.Driver):
continue
if tenant.get('id') != tenant_id:
continue
+ if trust_id is not None:
+ token_trust_id = token_ref_dict.get('trust_id')
+ if not token_trust_id:
+ continue
+ if token_trust_id != trust_id:
+ continue
tokens.append(token_ref['id'])
return tokens
diff --git a/keystone/token/controllers.py b/keystone/token/controllers.py
index d0538098..ade2af4f 100644
--- a/keystone/token/controllers.py
+++ b/keystone/token/controllers.py
@@ -21,7 +21,7 @@ class ExternalAuthNotApplicable(Exception):
pass
-@dependency.requires('catalog_api')
+@dependency.requires('catalog_api', 'trust_api', 'token_api')
class Auth(controller.V2Controller):
def ca_cert(self, context, auth=None):
ca_file = open(CONF.signing.ca_certs, 'r')
@@ -78,6 +78,7 @@ class Auth(controller.V2Controller):
context, auth)
user_ref, tenant_ref, metadata_ref, expiry = auth_info
+ trust_id = metadata_ref.get('trust_id')
user_ref = self._filter_domain_id(user_ref)
if tenant_ref:
tenant_ref = self._filter_domain_id(tenant_ref)
@@ -128,7 +129,8 @@ class Auth(controller.V2Controller):
expires=auth_token_data['expires'],
user=user_ref,
tenant=tenant_ref,
- metadata=metadata_ref))
+ metadata=metadata_ref,
+ trust_id=trust_id))
except Exception as e:
# an identical token may have been created already.
# if so, return the token_data as it is also identical
@@ -166,11 +168,43 @@ class Auth(controller.V2Controller):
except exception.NotFound as e:
raise exception.Unauthorized(e)
+ #A trust token cannot be used to get another token
+ if 'trust' in old_token_ref:
+ raise exception.Unauthorized()
+ if 'trust_id' in old_token_ref["metadata"]:
+ raise exception.Forbidden()
+
user_ref = old_token_ref['user']
user_id = user_ref['id']
+ if 'trust_id' in auth:
+ trust_ref = self.trust_api.get_trust(context, auth['trust_id'])
+ if trust_ref is None:
+ raise exception.Forbidden()
+ if user_id != trust_ref['trustee_user_id']:
+ raise exception.Forbidden()
+ if ('expires' in trust_ref) and (trust_ref['expires']):
+ expiry = trust_ref['expires']
+ if expiry < timeutils.parse_isotime(timeutils.isotime()):
+ raise exception.Forbidden()()
+ user_id = trust_ref['trustor_user_id']
+ trustor_user_ref = (self.identity_api.get_user(
+ context=context,
+ user_id=trust_ref['trustor_user_id']))
+ if not trustor_user_ref['enabled']:
+ raise exception.Forbidden()()
+ trustee_user_ref = self.identity_api.get_user(
+ context, trust_ref['trustee_user_id'])
+ if not trustee_user_ref['enabled']:
+ raise exception.Forbidden()()
+ if trust_ref['impersonation'] == 'True':
+ current_user_ref = trustor_user_ref
+ else:
+ current_user_ref = trustee_user_ref
- current_user_ref = self.identity_api.get_user(context=context,
- user_id=user_id)
+ else:
+ tenant_id = self._get_project_id_from_auth(context, auth)
+ current_user_ref = self.identity_api.get_user(context=context,
+ user_id=user_id)
tenant_id = self._get_project_id_from_auth(context, auth)
@@ -185,6 +219,28 @@ class Auth(controller.V2Controller):
context, user_id, tenant_id))
expiry = old_token_ref['expires']
+ if 'trust_id' in auth:
+ trust_id = auth['trust_id']
+ trust_roles = []
+ for role in trust_ref['roles']:
+ if not 'roles' in metadata_ref:
+ raise exception.Forbidden()()
+ if role['id'] in metadata_ref['roles']:
+ trust_roles.append(role['id'])
+ else:
+ raise exception.Forbidden()
+ if 'expiry' in trust_ref and trust_ref['expiry']:
+ trust_expiry = timeutils.parse_isotime(trust_ref['expiry'])
+ if trust_expiry < expiry:
+ expiry = trust_expiry
+ metadata_ref['roles'] = trust_roles
+ metadata_ref['trustee_user_id'] = trust_ref['trustee_user_id']
+ metadata_ref['trust_id'] = trust_id
+
+ auth_token_data = self._get_auth_token_data(current_user_ref,
+ tenant_ref,
+ metadata_ref,
+ expiry)
return (current_user_ref, tenant_ref, metadata_ref, expiry)
def _authenticate_local(self, context, auth):
@@ -526,7 +582,12 @@ class Auth(controller.V2Controller):
else:
o['access']['metadata'] = {'is_admin': 0}
if 'roles' in metadata_ref:
- o['access']['metadata']['roles'] = metadata_ref['roles']
+ o['access']['metadata']['roles'] = metadata_ref['roles']
+ if 'trust_id' in metadata_ref:
+ o['access']['trust'] = {'trustee_user_id':
+ metadata_ref['trustee_user_id'],
+ 'id': metadata_ref['trust_id']
+ }
return o
@classmethod
diff --git a/keystone/token/core.py b/keystone/token/core.py
index 4737f539..37ecffbc 100644
--- a/keystone/token/core.py
+++ b/keystone/token/core.py
@@ -179,11 +179,15 @@ class Driver(object):
"""
raise exception.NotImplemented()
- def list_tokens(self, user_id):
+ def list_tokens(self, user_id, tenant_id=None, trust_id=None):
"""Returns a list of current token_id's for a user
:param user_id: identity of the user
:type user_id: string
+ :param tenant_id: identity of the tenant
+ :type tenant_id: string
+ :param trust_id: identified of the trust
+ :type trust_id: string
:returns: list of token_id's
"""
diff --git a/keystone/trust/__init__.py b/keystone/trust/__init__.py
new file mode 100644
index 00000000..9c6a22f0
--- /dev/null
+++ b/keystone/trust/__init__.py
@@ -0,0 +1,19 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack LLC
+#
+# 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 keystone.trust.core import Manager, Driver
+from keystone.trust import controllers
+from keystone.trust import routers
diff --git a/keystone/trust/backends/__init__.py b/keystone/trust/backends/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/keystone/trust/backends/__init__.py
diff --git a/keystone/trust/backends/kvs.py b/keystone/trust/backends/kvs.py
new file mode 100644
index 00000000..ef528626
--- /dev/null
+++ b/keystone/trust/backends/kvs.py
@@ -0,0 +1,92 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack LLC
+#
+# 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.
+"""
+An in memory implementation of the trusts API.
+only to be used for testing purposes
+"""
+import copy
+import datetime
+
+
+from keystone.common import kvs
+from keystone.openstack.common import timeutils
+from keystone import exception
+from keystone import trust
+
+
+class Trust(kvs.Base, trust.Driver):
+ def create_trust(self, trust_id, trust, roles):
+ trust_ref = trust
+ trust_ref['id'] = trust_id
+ trust_ref['deleted'] = False
+ trust_ref['roles'] = roles
+ if (trust_ref.get('expires_at') and
+ trust_ref['expires_at'].tzinfo is not None):
+ trust_ref['expires_at'] = (timeutils.normalize_time
+ (trust_ref['expires_at']))
+
+ self.db.set('trust-%s' % trust_id, trust_ref)
+ trustee_user_id = trust_ref['trustee_user_id']
+ trustee_list = self.db.get('trustee-%s' % trustee_user_id, [])
+ trustee_list.append(trust_id)
+ self.db.set('trustee-%s' % trustee_user_id, trustee_list)
+ trustor_user_id = trust_ref['trustor_user_id']
+ trustor_list = self.db.get('trustor-%s' % trustor_user_id, [])
+ trustor_list.append(trust_id)
+ self.db.set('trustor-%s' % trustor_user_id, trustor_list)
+ return copy.deepcopy(trust_ref)
+
+ def _filter_trust(selfself, ref):
+ if ref['deleted']:
+ return None
+ if ref.get('expires_at') and timeutils.utcnow() > ref['expires_at']:
+ return None
+ ref = copy.deepcopy(ref)
+ return ref
+
+ def get_trust(self, trust_id):
+ try:
+ ref = self.db.get('trust-%s' % trust_id)
+ return self._filter_trust(ref)
+ except exception.NotFound:
+ return None
+
+ def delete_trust(self, trust_id):
+ try:
+ ref = self.db.get('trust-%s' % trust_id)
+ except exception.NotFound:
+ raise exception.TrustNotFound(token_id=token_id)
+ ref['deleted'] = True
+ self.db.set('trust-%s' % trust_id, ref)
+
+ def list_trusts(self):
+ trusts = []
+ for key, value in self.db.items():
+ if key.startswith("trust-") and not value['deleted']:
+ trusts.append(value)
+ return trusts
+
+ def list_trusts_for_trustee(self, trustee_user_id):
+ trusts = []
+ for trust in self.db.get('trustee-%s' % trustee_user_id, []):
+ trusts.append(self.get_trust(trust))
+ return trusts
+
+ def list_trusts_for_trustor(self, trustor_user_id):
+ trusts = []
+ for trust in self.db.get('trustor-%s' % trustor_user_id, []):
+ trusts.append(self.get_trust(trust))
+ return trusts
diff --git a/keystone/trust/backends/sql.py b/keystone/trust/backends/sql.py
new file mode 100644
index 00000000..dc3644e3
--- /dev/null
+++ b/keystone/trust/backends/sql.py
@@ -0,0 +1,123 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack LLC
+#
+# 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 keystone.common import sql
+from keystone import exception
+from keystone.openstack.common import timeutils
+from keystone import trust
+
+
+class TrustModel(sql.ModelBase, sql.DictBase):
+ __tablename__ = 'trust'
+ attributes = ['id', 'trustor_user_id', 'trustee_user_id',
+ 'project_id', 'impersonation', 'expires_at']
+ id = sql.Column(sql.String(64), primary_key=True)
+ #user id Of owner
+ trustor_user_id = sql.Column(sql.String(64), unique=False, nullable=False,)
+ #user_id of user allowed to consume this preauth
+ trustee_user_id = sql.Column(sql.String(64), unique=False, nullable=False)
+ project_id = sql.Column(sql.String(64), unique=False, nullable=True)
+ impersonation = sql.Column(sql.Boolean)
+ deleted_at = sql.Column(sql.DateTime)
+ expires_at = sql.Column(sql.DateTime)
+ extra = sql.Column(sql.JsonBlob())
+
+
+class TrustRole(sql.ModelBase):
+ __tablename__ = 'trust_role'
+ attributes = ['trust_id', 'role_id']
+ trust_id = sql.Column(sql.String(64), primary_key=True, nullable=False)
+ role_id = sql.Column(sql.String(64), primary_key=True, nullable=False)
+
+
+class Trust(sql.Base, trust.Driver):
+ @sql.handle_conflicts(type='trust')
+ def create_trust(self, trust_id, trust, roles):
+ session = self.get_session()
+ with session.begin():
+ ref = TrustModel.from_dict(trust)
+ ref['id'] = trust_id
+ if ref.get('expires_at') and ref['expires_at'].tzinfo is not None:
+ ref['expires_at'] = timeutils.normalize_time(ref['expires_at'])
+ session.add(ref)
+ added_roles = []
+ for role in roles:
+ trust_role = TrustRole()
+ trust_role.trust_id = trust_id
+ trust_role.role_id = role['id']
+ added_roles.append({'id': role['id']})
+ session.add(trust_role)
+ session.flush()
+ trust_dict = ref.to_dict()
+ trust_dict['roles'] = added_roles
+ return trust_dict
+
+ def _add_roles(self, trust_id, session, trust_dict):
+ roles = []
+ for role in session.query(TrustRole).filter_by(trust_id=trust_id):
+ roles.append({'id': role.role_id})
+ trust_dict['roles'] = roles
+
+ @sql.handle_conflicts(type='trust')
+ def get_trust(self, trust_id):
+ session = self.get_session()
+ ref = (session.query(TrustModel).
+ filter_by(deleted_at=None).
+ filter_by(id=trust_id).first())
+ if ref is None:
+ return None
+ if ref.expires_at is not None:
+ now = timeutils.utcnow()
+ if now > ref.expires_at:
+ return None
+ trust_dict = ref.to_dict()
+
+ self._add_roles(trust_id, session, trust_dict)
+ return trust_dict
+
+ @sql.handle_conflicts(type='trust')
+ def list_trusts(self):
+ session = self.get_session()
+ trusts = session.query(TrustModel).filter_by(deleted_at=None)
+ return [trust_ref.to_dict() for trust_ref in trusts]
+
+ @sql.handle_conflicts(type='trust')
+ def list_trusts_for_trustee(self, trustee_user_id):
+ session = self.get_session()
+ trusts = (session.query(TrustModel).
+ filter_by(deleted_at=None).
+ filter_by(trustee_user_id=trustee_user_id))
+ return [trust_ref.to_dict() for trust_ref in trusts]
+
+ @sql.handle_conflicts(type='trust')
+ def list_trusts_for_trustor(self, trustor_user_id):
+ session = self.get_session()
+ trusts = (session.query(TrustModel).
+ filter_by(deleted_at=None).
+ filter_by(trustor_user_id=trustor_user_id))
+ return [trust_ref.to_dict() for trust_ref in trusts]
+
+ @sql.handle_conflicts(type='trust')
+ def delete_trust(self, trust_id):
+ session = self.get_session()
+ with session.begin():
+ try:
+ trust_ref = (session.query(TrustModel).
+ filter_by(id=trust_id).one())
+ except sql.NotFound:
+ raise exception.TrustNotFound(trust_id=trust_id)
+ trust_ref.deleted_at = timeutils.utcnow()
+ session.flush()
diff --git a/keystone/trust/controllers.py b/keystone/trust/controllers.py
new file mode 100644
index 00000000..00183bc5
--- /dev/null
+++ b/keystone/trust/controllers.py
@@ -0,0 +1,244 @@
+import uuid
+import json
+
+from keystone import config
+from keystone import exception
+from keystone import identity
+from keystone.common import controller
+from keystone.common import dependency
+from keystone.common import logging
+from keystone import exception
+from keystone.openstack.common import timeutils
+
+
+LOG = logging.getLogger(__name__)
+CONF = config.CONF
+
+
+def _trustor_only(context, trust, user_id):
+ if user_id != trust.get('trustor_user_id'):
+ raise exception.Forbidden()
+
+
+def _admin_trustor_trustee_only(context, trust, user_id):
+ if (user_id != trust.get('trustor_user_id') and
+ user_id != trust.get('trustor_user_id') and
+ context['is_admin']):
+ raise exception.Forbidden()
+
+
+def _admin_trustor_only(context, trust, user_id):
+ if user_id != trust.get('trustor_user_id') and not context['is_admin']:
+ raise exception.Forbidden()
+
+
+@dependency.requires('identity_api', 'trust_api', 'token_api')
+class TrustV3(controller.V3Controller):
+ collection_name = "trusts"
+ member_name = "trust"
+
+ def _get_user_id(self, context):
+ if 'token_id' in context:
+ token_id = context['token_id']
+ token = self.token_api.get_token(context, token_id)
+ user_id = token['user']['id']
+ return user_id
+ return None
+
+ def get_trust(self, context, trust_id):
+ user_id = self._get_user_id(context)
+ trust = self.trust_api.get_trust(context, trust_id)
+ if not trust:
+ raise exception.TrustNotFound(trust_id)
+ _admin_trustor_trustee_only(context, trust, user_id)
+ if not trust:
+ raise exception.TrustNotFound(trust_id=trust_id)
+ if (user_id != trust['trustor_user_id'] and
+ user_id != trust['trustee_user_id']):
+ raise exception.Forbidden()
+ self._fill_in_roles(context, trust,
+ self.identity_api.list_roles(context))
+ return TrustV3.wrap_member(context, trust)
+
+ def _fill_in_roles(self, context, trust, global_roles):
+ if trust.get('expires_at') is not None:
+ trust['expires_at'] = (timeutils.isotime
+ (trust['expires_at'],
+ subsecond=True))
+
+ if not 'roles' in trust:
+ trust['roles'] = []
+ trust_full_roles = []
+ for trust_role in trust['roles']:
+ if isinstance(trust_role, basestring):
+ trust_role = {'id': trust_role}
+ matching_roles = [x for x in global_roles
+ if x['id'] == trust_role['id']]
+ if matching_roles:
+ full_role = identity.controllers.RoleV3.wrap_member(
+ context, matching_roles[0])['role']
+ trust_full_roles.append(full_role)
+ trust['roles'] = trust_full_roles
+ trust['roles_links'] = {
+ 'self': (CONF.public_endpoint % CONF +
+ "trusts/%s/roles" % trust['id']),
+ 'next': None,
+ 'previous': None}
+
+ def _clean_role_list(self, context, trust, global_roles):
+ trust_roles = []
+ global_role_names = dict((r['name'], r)
+ for r in
+ global_roles)
+ for role in trust.get('roles', []):
+ if 'id' in role:
+ trust_roles.append({'id': role['id']})
+ elif 'name' in role:
+ rolename = role['name']
+ if rolename in global_role_names:
+ trust_roles.append({'id':
+ global_role_names[rolename]['id']})
+ else:
+ raise exception.RoleNotFound("role %s is not defined" %
+ rolename)
+ else:
+ raise exception.ValidationError(attribute='id or name',
+ target='roles')
+ return trust_roles
+
+ @controller.protected
+ def create_trust(self, context, trust=None):
+ """
+ The user creating the trust must be trustor
+ """
+
+ #TODO instead of raising ValidationError on the first problem,
+ #return a collection of all the problems.
+ if not trust:
+ raise exception.ValidationError(attribute='trust',
+ target='request')
+ try:
+ user_id = self._get_user_id(context)
+ _trustor_only(context, trust, user_id)
+ #confirm that the trustee exists
+ trustee_ref = self.identity_api.get_user(context,
+ trust['trustee_user_id'])
+ if not trustee_ref:
+ raise exception.UserNotFound(user_id=trust['trustee_user_id'])
+ global_roles = self.identity_api.list_roles(context)
+ clean_roles = self._clean_role_list(context, trust, global_roles)
+ if trust.get('project_id'):
+ user_roles = self.identity_api.get_roles_for_user_and_project(
+ context, user_id, trust['project_id'])
+ else:
+ user_roles = []
+ for trust_role in clean_roles:
+ matching_roles = [x for x in user_roles
+ if x == trust_role['id']]
+ if not matching_roles:
+ raise exception.RoleNotFound(role_id=trust_role['id'])
+ if trust.get('expires_at') is not None:
+ if not trust['expires_at'].endswith('Z'):
+ trust['expires_at'] += 'Z'
+ trust['expires_at'] = (timeutils.parse_isotime
+ (trust['expires_at']))
+ new_trust = self.trust_api.create_trust(
+ context=context,
+ trust_id=uuid.uuid4().hex,
+ trust=trust,
+ roles=clean_roles)
+ self._fill_in_roles(context,
+ new_trust,
+ global_roles)
+ return TrustV3.wrap_member(context, new_trust)
+ except KeyError as e:
+ raise exception.ValidationError(attribute=e.args[0],
+ target='trust')
+
+ @controller.protected
+ def list_trusts(self, context):
+ query = context['query_string']
+ trusts = []
+ if not query:
+ self.assert_admin(context)
+ trusts += self.trust_api.list_trusts(context)
+ if 'trustor_user_id' in query:
+ user_id = query['trustor_user_id']
+ calling_user_id = self._get_user_id(context)
+ if user_id != calling_user_id:
+ raise exception.Forbidden()
+ trusts += (self.trust_api.
+ list_trusts_for_trustor(context, user_id))
+ if 'trustee_user_id' in query:
+ user_id = query['trustee_user_id']
+ calling_user_id = self._get_user_id(context)
+ if user_id != calling_user_id:
+ raise exception.Forbidden()
+ trusts += (self.trust_api.
+ list_trusts_for_trustee(context, user_id))
+ global_roles = self.identity_api.list_roles(context)
+ for trust in trusts:
+ self._fill_in_roles(context, trust, global_roles)
+ return TrustV3.wrap_collection(context, trusts)
+
+ @controller.protected
+ def delete_trust(self, context, trust_id):
+ trust = self.trust_api.get_trust(context, trust_id)
+ if not trust:
+ raise exception.TrustNotFound(trust_id)
+
+ user_id = self._get_user_id(context)
+ _admin_trustor_only(context, trust, user_id)
+ self.trust_api.delete_trust(context, trust_id)
+ userid = trust['trustor_user_id']
+ token_list = self.token_api.list_tokens(context,
+ userid,
+ trust_id=trust_id)
+ for token in token_list:
+ self.token_api.delete_token(context, token)
+
+ @controller.protected
+ def list_roles_for_trust(self, context, trust_id):
+ trust = self.get_trust(context, trust_id)['trust']
+ if not trust:
+ raise exception.TrustNotFound(trust_id)
+ user_id = self._get_user_id(context)
+ _admin_trustor_trustee_only(context, trust, user_id)
+ return {'roles': trust['roles'],
+ 'links': trust['roles_links']}
+
+ @controller.protected
+ def check_role_for_trust(self, context, trust_id, role_id):
+ """Checks if a role has been assigned to a trust."""
+ trust = self.trust_api.get_trust(context, trust_id)
+ if not trust:
+ raise exception.TrustNotFound(trust_id)
+ user_id = self._get_user_id(context)
+ _admin_trustor_trustee_only(context, trust, user_id)
+ matching_roles = [x for x in trust['roles']
+ if x['id'] == role_id]
+ if not matching_roles:
+ raise exception.RoleNotFound(role_id=role_id)
+
+ @controller.protected
+ def get_role_for_trust(self, context, trust_id, role_id):
+ """Checks if a role has been assigned to a trust."""
+ trust = self.trust_api.get_trust(context, trust_id)
+ if not trust:
+ raise exception.TrustNotFound(trust_id)
+
+ user_id = self._get_user_id(context)
+ _admin_trustor_trustee_only(context, trust, user_id)
+ matching_roles = [x for x in trust['roles']
+ if x['id'] == role_id]
+ if not matching_roles:
+ raise exception.RoleNotFound(role_id=role_id)
+ global_roles = self.identity_api.list_roles(context)
+ matching_roles = [x for x in global_roles
+ if x['id'] == role_id]
+ if matching_roles:
+ full_role = (identity.controllers.
+ RoleV3.wrap_member(context, matching_roles[0]))
+ return full_role
+ else:
+ raise exception.RoleNotFound(role_id=role_id)
diff --git a/keystone/trust/core.py b/keystone/trust/core.py
new file mode 100644
index 00000000..a9a15ff7
--- /dev/null
+++ b/keystone/trust/core.py
@@ -0,0 +1,63 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack LLC
+#
+# 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.
+
+"""Main entry point into the Identity service."""
+
+from keystone.common import dependency
+from keystone.common import logging
+from keystone.common import manager
+from keystone.common import wsgi
+from keystone import config
+from keystone import exception
+
+
+CONF = config.CONF
+
+LOG = logging.getLogger(__name__)
+
+
+@dependency.provider('trust_api')
+class Manager(manager.Manager):
+ """Default pivot point for the Trust backend.
+
+ See :mod:`keystone.common.manager.Manager` for more details on how this
+ dynamically calls the backend.
+
+ """
+
+ def __init__(self):
+ super(Manager, self).__init__(CONF.trust.driver)
+
+
+class Driver(object):
+ def create_trust(self, trust_id, trust, roles):
+ """Create a new trust.
+
+ :returns: a new trust
+ """
+ raise exception.NotImplemented()
+
+ def get_trust(self, trust_id):
+ raise exception.NotImplemented()
+
+ def list_trusts(self):
+ raise exception.NotImplemented()
+
+ def list_trusts_for_trustee(self, trustee):
+ raise exception.NotImplemented()
+
+ def list_trusts_for_trustor(self, trustor):
+ raise exception.NotImplemented()
diff --git a/keystone/trust/routers.py b/keystone/trust/routers.py
new file mode 100644
index 00000000..2ed35ed0
--- /dev/null
+++ b/keystone/trust/routers.py
@@ -0,0 +1,58 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack LLC
+#
+# 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.
+"""WSGI Routers for the Identity service."""
+from keystone.common import wsgi
+from keystone.trust import controllers
+from keystone.common import router
+
+
+def append_v3_routers(mapper, routers):
+ trust_controller = controllers.TrustV3()
+
+ mapper.connect('/trusts',
+ controller=trust_controller,
+ action='create_trust',
+ conditions=dict(method=['POST']))
+
+ mapper.connect('/trusts',
+ controller=trust_controller,
+ action='list_trusts',
+ conditions=dict(method=['GET']))
+
+ mapper.connect('/trusts/{trust_id}',
+ controller=trust_controller,
+ action='delete_trust',
+ conditions=dict(method=['DELETE']))
+
+ mapper.connect('/trusts/{trust_id}',
+ controller=trust_controller,
+ action='get_trust',
+ conditions=dict(method=['GET']))
+
+ mapper.connect('/trusts/{trust_id}/roles',
+ controller=trust_controller,
+ action='list_roles_for_trust',
+ conditions=dict(method=['GET']))
+
+ mapper.connect('/trusts/{trust_id}/roles/{role_id}',
+ controller=trust_controller,
+ action='check_role_for_trust',
+ conditions=dict(method=['HEAD']))
+
+ mapper.connect('/trusts/{trust_id}/roles/{role_id}',
+ controller=trust_controller,
+ action='get_role_for_trust',
+ conditions=dict(method=['GET']))
diff --git a/tests/backend_sql.conf b/tests/backend_sql.conf
index e1027cc8..0baf610c 100644
--- a/tests/backend_sql.conf
+++ b/tests/backend_sql.conf
@@ -22,3 +22,6 @@ driver = keystone.catalog.backends.sql.Catalog
[policy]
driver = keystone.policy.backends.sql.Policy
+
+[trust]
+driver = keystone.trust.backends.sql.Trust
diff --git a/tests/default_fixtures.py b/tests/default_fixtures.py
index 4499be17..1141ddfd 100644
--- a/tests/default_fixtures.py
+++ b/tests/default_fixtures.py
@@ -109,5 +109,12 @@ ROLES = [
}, {
'id': 'other',
'name': 'Other',
+ }, {
+ 'id': 'browser',
+ 'name': 'Browser',
+ }, {
+ 'id': 'writer',
+ 'name': 'Writer',
}
+
]
diff --git a/tests/test_auth.py b/tests/test_auth.py
index ae9bdac3..8c8feba0 100644
--- a/tests/test_auth.py
+++ b/tests/test_auth.py
@@ -12,23 +12,29 @@
# License for the specific language governing permissions and limitations
# under the License.
+import copy
+import datetime
import time
import uuid
+from keystone import auth
from keystone import config
from keystone import exception
from keystone.openstack.common import timeutils
from keystone import test
from keystone import token
+from keystone import trust
import default_fixtures
CONF = config.CONF
+TIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'
def _build_user_auth(token=None, user_id=None, username=None,
- password=None, tenant_id=None, tenant_name=None):
+ password=None, tenant_id=None, tenant_name=None,
+ trust_id=None):
"""Build auth dictionary.
It will create an auth dictionary based on all the arguments
@@ -49,6 +55,8 @@ def _build_user_auth(token=None, user_id=None, username=None,
auth_json['tenantName'] = tenant_name
if tenant_id is not None:
auth_json['tenantId'] = tenant_id
+ if trust_id is not None:
+ auth_json['trust_id'] = trust_id
return auth_json
@@ -452,6 +460,240 @@ class AuthWithRemoteUser(AuthTest):
body_dict)
+class AuthWithTrust(AuthTest):
+ def setUp(self):
+ super(AuthWithTrust, self).setUp()
+
+ trust.Manager()
+ self.trust_controller = trust.controllers.TrustV3()
+ self.auth_v3_controller = auth.controllers.Auth()
+ self.trustor = self.user_foo
+ self.trustee = self.user_two
+ self.assigned_roles = [self.role_member['id'],
+ self.role_browser['id']]
+ for assigned_role in self.assigned_roles:
+ self.identity_api.add_role_to_user_and_project(
+ self.trustor['id'], self.tenant_bar['id'], assigned_role)
+
+ self.sample_data = {'trustor_user_id': self.trustor['id'],
+ 'trustee_user_id': self.trustee['id'],
+ 'project_id': self.tenant_bar['id'],
+ 'impersonation': 'True',
+ 'roles': [{'id': self.role_browser['id']},
+ {'name': self.role_member['name']}]}
+ expires_at = timeutils.strtime(timeutils.utcnow() +
+ datetime.timedelta(minutes=10),
+ fmt=TIME_FORMAT)
+ self.create_trust(expires_at=expires_at)
+
+ def create_trust(self, expires_at=None, impersonation='True'):
+ username = self.trustor['name'],
+ password = 'foo2'
+ body_dict = _build_user_auth(username=username, password=password)
+ self.unscoped_token = self.controller.authenticate({}, body_dict)
+ context = {'token_id': self.unscoped_token['access']['token']['id']}
+ trust_data = copy.deepcopy(self.sample_data)
+ trust_data['expires_at'] = expires_at
+ trust_data['impersonation'] = impersonation
+
+ self.new_trust = (self.trust_controller.create_trust
+ (context, trust=trust_data)['trust'])
+
+ def build_v2_token_request(self, username, password):
+ body_dict = _build_user_auth(username=username, password=password)
+ self.unscoped_token = self.controller.authenticate({}, body_dict)
+ unscoped_token_id = self.unscoped_token['access']['token']['id']
+ request_body = _build_user_auth(token={'id': unscoped_token_id},
+ trust_id=self.new_trust['id'],
+ tenant_id=self.tenant_bar['id'])
+ return request_body
+
+ def test_create_trust_bad_data_fails(self):
+ context = {'token_id': self.unscoped_token['access']['token']['id']}
+ bad_sample_data = {'trustor_user_id': self.trustor['id']}
+
+ self.assertRaises(exception.ValidationError,
+ self.trust_controller.create_trust,
+ context, trust=bad_sample_data)
+
+ def test_create_trust_no_roles(self):
+ self.new_trust = None
+ self.sample_data['roles'] = []
+ self.create_trust()
+ self.assertEquals(self.new_trust['roles'], [])
+
+ def test_create_trust(self):
+ self.assertEquals(self.new_trust['trustor_user_id'],
+ self.trustor['id'])
+ self.assertEquals(self.new_trust['trustee_user_id'],
+ self.trustee['id'])
+ role_ids = [self.role_browser['id'], self.role_member['id']]
+ self.assertTrue(timeutils.parse_strtime(self.new_trust['expires_at'],
+ fmt=TIME_FORMAT))
+
+ for role in self.new_trust['roles']:
+ self.assertIn(role['id'], role_ids)
+
+ def test_get_trust(self):
+ context = {'token_id': self.unscoped_token['access']['token']['id']}
+ trust = self.trust_controller.get_trust(context,
+ self.new_trust['id'])['trust']
+ self.assertEquals(trust['trustor_user_id'],
+ self.trustor['id'])
+ self.assertEquals(trust['trustee_user_id'],
+ self.trustee['id'])
+ role_ids = [self.role_browser['id'], self.role_member['id']]
+ for role in self.new_trust['roles']:
+ self.assertIn(role['id'], role_ids)
+
+ def test_create_trust_no_impersonation(self):
+ self.create_trust(expires_at=None, impersonation='False')
+ self.assertEquals(self.new_trust['trustor_user_id'],
+ self.trustor['id'])
+ self.assertEquals(self.new_trust['trustee_user_id'],
+ self.trustee['id'])
+ self.assertEquals(self.new_trust['impersonation'],
+ 'False')
+ auth_response = self.fetch_v2_token_from_trust()
+ token_user = auth_response['access']['user']
+ self.assertEquals(token_user['id'],
+ self.new_trust['trustee_user_id'])
+
+ #TODO Endpoints
+
+ def test_token_from_trust_wrong_user_fails(self):
+ new_trust = self.create_trust()
+ request_body = self.build_v2_token_request('FOO', 'foo2')
+ self.assertRaises(
+ exception.Forbidden,
+ self.controller.authenticate, {}, request_body)
+
+ def fetch_v2_token_from_trust(self):
+ request_body = self.build_v2_token_request('TWO', 'two2')
+ auth_response = self.controller.authenticate({}, request_body)
+ return auth_response
+
+ def fetch_v3_token_from_trust(self):
+ self.identity_api.create_domain("default",
+ {"name": "default",
+ "id": "default"})
+ v3_password_data = {
+ 'identity': {
+ "methods": ["password"],
+ "password": {
+ "user": {
+ "id": self.trustee["id"],
+ "password": self.trustee["password"]}}
+ },
+ 'scope': {
+ 'project': {
+ 'id': self.tenant_baz['id']}}}
+ auth_response = (self.auth_v3_controller.authenticate_for_token
+ ({}, v3_password_data))
+ token = auth_response.headers['X-Subject-Token']
+
+ v3_req_with_trust = {
+ "identity": {
+ "methods": ["token"],
+ "token": {"id": token}},
+ "scope": {
+ "trust": {"id": self.new_trust['id']}}}
+ token_auth_response = (self.auth_v3_controller.authenticate_for_token
+ ({}, v3_req_with_trust))
+ return token_auth_response
+
+ def test_create_v3_token_from_trust(self):
+ auth_response = self.fetch_v3_token_from_trust()
+
+ trust_token_user = auth_response.json['token']['user']
+ self.assertEquals(trust_token_user['id'], self.trustor['id'])
+
+ trust_token_trust = auth_response.json['token']['trust']
+ self.assertEquals(trust_token_trust['id'], self.new_trust['id'])
+ self.assertEquals(trust_token_trust['trustor_user']['id'],
+ self.trustor['id'])
+ self.assertEquals(trust_token_trust['trustee_user']['id'],
+ self.trustee['id'])
+
+ trust_token_roles = auth_response.json['token']['roles']
+ self.assertEquals(len(trust_token_roles), 2)
+
+ def test_v3_trust_token_get_token_fails(self):
+ auth_response = self.fetch_v3_token_from_trust()
+ trust_token = auth_response.headers['X-Subject-Token']
+ v3_token_data = {
+ "methods": ["token"],
+ "token": {"id": trust_token}
+ }
+ self.assertRaises(
+ exception.Unauthorized,
+ self.auth_v3_controller.authenticate_for_token,
+ {}, v3_token_data)
+
+ def test_token_from_trust(self):
+ auth_response = self.fetch_v2_token_from_trust()
+
+ self.assertIsNotNone(auth_response)
+ self.assertEquals(len(auth_response['access']['metadata']['roles']),
+ 2,
+ "user_foo has three roles, but the token should"
+ " only get the two roles specified in the trust.")
+
+ def test_token_from_trust_cant_get_another_token(self):
+ auth_response = self.fetch_v2_token_from_trust()
+ trust_token_id = auth_response['access']['token']['id']
+ request_body = _build_user_auth(token={'id': trust_token_id},
+ tenant_id=self.tenant_bar['id'])
+ self.assertRaises(
+ exception.Forbidden,
+ self.controller.authenticate, {}, request_body)
+
+ def test_delete_trust_revokes_token(self):
+ context = {'token_id': self.unscoped_token['access']['token']['id']}
+ auth_response = self.fetch_v2_token_from_trust()
+ trust_id = self.new_trust['id']
+ trust_token_id = auth_response['access']['token']['id']
+ tokens = self.token_api.list_tokens(self.trustor['id'],
+ trust_id=trust_id)
+ self.assertEquals(len(tokens), 1)
+ self.trust_controller.delete_trust(context, trust_id=trust_id)
+ tokens = self.token_api.list_tokens(self.trustor['id'],
+ trust_id=trust_id)
+ self.assertEquals(len(tokens), 0)
+
+ def test_token_from_trust_with_no_role_fails(self):
+ for assigned_role in self.assigned_roles:
+ self.identity_api.remove_role_from_user_and_project(
+ self.trustor['id'], self.tenant_bar['id'], assigned_role)
+ request_body = self.build_v2_token_request('TWO', 'two2')
+ self.assertRaises(
+ exception.Forbidden,
+ self.controller.authenticate, {}, request_body)
+
+ def test_expired_trust_get_token_fails(self):
+ expiry = "1999-02-18T10:10:00Z"
+ self.create_trust(expiry)
+ request_body = self.build_v2_token_request('TWO', 'two2')
+ self.assertRaises(
+ exception.Forbidden,
+ self.controller.authenticate, {}, request_body)
+
+ def test_token_from_trust_with_wrong_role_fails(self):
+ self.identity_api.add_role_to_user_and_project(
+ self.trustor['id'],
+ self.tenant_bar['id'],
+ self.role_other['id'])
+ for assigned_role in self.assigned_roles:
+ self.identity_api.remove_role_from_user_and_project(
+ self.trustor['id'], self.tenant_bar['id'], assigned_role)
+
+ request_body = self.build_v2_token_request('TWO', 'two2')
+
+ self.assertRaises(
+ exception.Forbidden,
+ self.controller.authenticate, {}, request_body)
+
+
class TokenExpirationTest(AuthTest):
def _maintain_token_expiration(self):
"""Token expiration should be maintained after re-auth & validation."""
diff --git a/tests/test_backend.py b/tests/test_backend.py
index 029901eb..1af0822c 100644
--- a/tests/test_backend.py
+++ b/tests/test_backend.py
@@ -29,6 +29,7 @@ from keystone import test
CONF = config.CONF
DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id
+TIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'
class IdentityTests(object):
@@ -1888,12 +1889,14 @@ class TokenTests(object):
self.assertRaises(exception.TokenNotFound,
self.token_api.delete_token, token_id)
- def create_token_sample_data(self, tenant_id=None):
+ def create_token_sample_data(self, tenant_id=None, trust_id=None):
token_id = uuid.uuid4().hex
data = {'id': token_id, 'a': 'b',
'user': {'id': 'testuserid'}}
if tenant_id is not None:
data['tenant'] = {'id': tenant_id, 'name': tenant_id}
+ if trust_id is not None:
+ data['trust_id'] = trust_id
self.token_api.create_token(token_id, data)
return token_id
@@ -1936,6 +1939,13 @@ class TokenTests(object):
self.assertNotIn(token_id3, tokens)
self.assertIn(token_id4, tokens)
+ def test_token_list_trust(self):
+ trust_id = uuid.uuid4().hex
+ token_id5 = self.create_token_sample_data(trust_id=trust_id)
+ tokens = self.token_api.list_tokens('testuserid', trust_id=trust_id)
+ self.assertEquals(len(tokens), 1)
+ self.assertIn(token_id5, tokens)
+
def test_get_token_404(self):
self.assertRaises(exception.TokenNotFound,
self.token_api.get_token,
@@ -1965,7 +1975,7 @@ class TokenTests(object):
data = {'id': token_id, 'id_hash': token_id, 'a': 'b', 'expires': None,
'user': {'id': 'testuserid'}}
data_ref = self.token_api.create_token(token_id, data)
- self.assertDictEqual(data_ref, data)
+ self.assertIsNotNone(data_ref['expires'])
new_data_ref = self.token_api.get_token(token_id)
self.assertEqual(data_ref, new_data_ref)
@@ -2002,6 +2012,80 @@ class TokenTests(object):
for x in xrange(2)])
+class TrustTests(object):
+ def create_sample_trust(self, new_id):
+ self.trustor = self.user_foo
+ self.trustee = self.user_two
+ trust_data = (self.trust_api.create_trust
+ (new_id,
+ {'trustor_user_id': self.trustor['id'],
+ 'trustee_user_id': self.user_two['id'],
+ 'project_id': self.tenant_bar['id'],
+ 'expires_at': timeutils.
+ parse_isotime('2031-02-18T18:10:00Z'),
+ 'impersonation': True},
+ roles=[{"id": "member"},
+ {"id": "other"},
+ {"id": "browser"}]))
+ return trust_data
+
+ def test_delete_trust(self):
+ new_id = uuid.uuid4().hex
+ trust_data = self.create_sample_trust(new_id)
+ trust_id = trust_data['id']
+ self.assertIsNotNone(trust_data)
+ trust_data = self.trust_api.get_trust(trust_id)
+ self.assertEquals(new_id, trust_data['id'])
+ self.trust_api.delete_trust(trust_id)
+ self.assertIsNone(self.trust_api.get_trust(trust_id))
+
+ def test_get_trust(self):
+ new_id = uuid.uuid4().hex
+ trust_data = self.create_sample_trust(new_id)
+ trust_id = trust_data['id']
+ self.assertIsNotNone(trust_data)
+ trust_data = self.trust_api.get_trust(trust_id)
+ self.assertEquals(new_id, trust_data['id'])
+
+ def test_create_trust(self):
+ new_id = uuid.uuid4().hex
+ trust_data = self.create_sample_trust(new_id)
+
+ self.assertEquals(new_id, trust_data['id'])
+ self.assertEquals(self.trustee['id'], trust_data['trustee_user_id'])
+ self.assertEquals(self.trustor['id'], trust_data['trustor_user_id'])
+ self.assertTrue(timeutils.normalize_time(trust_data['expires_at']) >
+ timeutils.utcnow())
+
+ self.assertEquals([{'id':'member'},
+ {'id': 'other'},
+ {'id': 'browser'}], trust_data['roles'])
+
+ def test_list_trust_by_trustee(self):
+ for i in range(0, 3):
+ trust_data = self.create_sample_trust(uuid.uuid4().hex)
+ trusts = self.trust_api.list_trusts_for_trustee(self.trustee)
+ self.assertEqual(len(trusts), 3)
+ self.assertEqual(trusts[0]["trustee_user_id"], self.trustee['id'])
+ trusts = self.trust_api.list_trusts_for_trustee(self.trustor)
+ self.assertEqual(len(trusts), 0)
+
+ def test_list_trust_by_trustee(self):
+ for i in range(0, 3):
+ trust_data = self.create_sample_trust(uuid.uuid4().hex)
+ trusts = self.trust_api.list_trusts_for_trustor(self.trustor['id'])
+ self.assertEqual(len(trusts), 3)
+ self.assertEqual(trusts[0]["trustor_user_id"], self.trustor['id'])
+ trusts = self.trust_api.list_trusts_for_trustor(self.trustee['id'])
+ self.assertEqual(len(trusts), 0)
+
+ def test_list_trusts(self):
+ for i in range(0, 3):
+ trust_data = self.create_sample_trust(uuid.uuid4().hex)
+ trusts = self.trust_api.list_trusts()
+ self.assertEqual(len(trusts), 3)
+
+
class CommonHelperTests(test.TestCase):
def test_format_helper_raises_malformed_on_missing_key(self):
with self.assertRaises(exception.MalformedEndpoint):
diff --git a/tests/test_backend_kvs.py b/tests/test_backend_kvs.py
index e1d99d47..74b5e4eb 100644
--- a/tests/test_backend_kvs.py
+++ b/tests/test_backend_kvs.py
@@ -22,6 +22,7 @@ from keystone import exception
from keystone.identity.backends import kvs as identity_kvs
from keystone import test
from keystone.token.backends import kvs as token_kvs
+from keystone.trust.backends import kvs as trust_kvs
import default_fixtures
import test_backend
@@ -71,6 +72,15 @@ class KvsToken(test.TestCase, test_backend.TokenTests):
self.token_api = token_kvs.Token(db={})
+class KvsTrust(test.TestCase, test_backend.TrustTests):
+ def setUp(self):
+ super(KvsTrust, self).setUp()
+ self.trust_api = trust_kvs.Trust(db={})
+ self.identity_api = identity_kvs.Identity(db={})
+ self.catalog_api = catalog_kvs.Catalog(db={})
+ self.load_fixtures(default_fixtures)
+
+
class KvsCatalog(test.TestCase, test_backend.CatalogTests):
def setUp(self):
super(KvsCatalog, self).setUp()
diff --git a/tests/test_backend_sql.py b/tests/test_backend_sql.py
index a4be85e3..04310307 100644
--- a/tests/test_backend_sql.py
+++ b/tests/test_backend_sql.py
@@ -24,6 +24,8 @@ from keystone import identity
from keystone import policy
from keystone import test
from keystone import token
+from keystone import trust
+
import default_fixtures
import test_backend
@@ -43,6 +45,7 @@ class SqlTests(test.TestCase):
self.catalog_man = catalog.Manager()
self.identity_man = identity.Manager()
self.token_man = token.Manager()
+ self.trust_man = trust.Manager()
self.policy_man = policy.Manager()
# create shortcut references to each driver
@@ -50,6 +53,7 @@ class SqlTests(test.TestCase):
self.identity_api = self.identity_man.driver
self.token_api = self.token_man.driver
self.policy_api = self.policy_man.driver
+ self.trust_api = self.trust_man.driver
# populate the engine with tables & fixtures
self.load_fixtures(default_fixtures)
@@ -221,6 +225,10 @@ class SqlIdentity(SqlTests, test_backend.IdentityTests):
self.assertEqual(arbitrary_value, ref['extra'][arbitrary_key])
+class SqlTrust(SqlTests, test_backend.TrustTests):
+ pass
+
+
class SqlToken(SqlTests, test_backend.TokenTests):
pass
diff --git a/tests/test_content_types.py b/tests/test_content_types.py
index 91b61661..e0b42db9 100644
--- a/tests/test_content_types.py
+++ b/tests/test_content_types.py
@@ -90,7 +90,7 @@ class RestfulTestCase(test.TestCase):
# Initialize headers dictionary
headers = {} if not headers else headers
- connection = httplib.HTTPConnection(host, port, timeout=10)
+ connection = httplib.HTTPConnection(host, port, timeout=100000)
# Perform the request
connection.request(method, path, body, headers)
diff --git a/tests/test_overrides.conf b/tests/test_overrides.conf
index 48f5dd7f..0e41fd32 100644
--- a/tests/test_overrides.conf
+++ b/tests/test_overrides.conf
@@ -8,6 +8,9 @@ driver = keystone.identity.backends.kvs.Identity
driver = keystone.catalog.backends.templated.TemplatedCatalog
template_file = default_catalog.templates
+[trust]
+driver = keystone.trust.backends.kvs.Trust
+
[signing]
certfile = ../examples/pki/certs/signing_cert.pem
keyfile = ../examples/pki/private/signing_key.pem
diff --git a/tests/test_sql_upgrade.py b/tests/test_sql_upgrade.py
index 85ea7580..003fb4ec 100644
--- a/tests/test_sql_upgrade.py
+++ b/tests/test_sql_upgrade.py
@@ -526,6 +526,18 @@ class SqlUpgradeTests(test.TestCase):
cmd = this_table.delete(id=project['id'])
self.engine.execute(cmd)
+ def test_upgrade_trusts(self):
+ self.assertEqual(self.schema.version, 0, "DB is at version 0")
+ self.upgrade(18)
+ self.assertTableColumns("trust",
+ ["id", "trustor_user_id",
+ "trustee_user_id",
+ "project_id", "impersonation",
+ "deleted_at",
+ "expires_at", "extra"])
+ self.assertTableColumns("trust_role",
+ ["trust_id", "role_id"])
+
def populate_user_table(self, with_pass_enab=False,
with_pass_enab_domain=False):
# Populate the appropriate fields in the user
diff --git a/tests/test_v3.py b/tests/test_v3.py
index a0252af0..2aa2c2b5 100644
--- a/tests/test_v3.py
+++ b/tests/test_v3.py
@@ -7,6 +7,8 @@ from keystone.common.sql import util as sql_util
from keystone import auth
from keystone import test
from keystone import config
+from keystone.policy.backends import rules
+
import test_content_types
@@ -16,11 +18,14 @@ CONF = config.CONF
class RestfulTestCase(test_content_types.RestfulTestCase):
def setUp(self):
+ rules.reset()
+
self.config([
test.etcdir('keystone.conf.sample'),
test.testsdir('test_overrides.conf'),
test.testsdir('backend_sql.conf'),
test.testsdir('backend_sql_disk.conf')])
+
sql_util.setup_test_database()
self.load_backends()
@@ -62,6 +67,9 @@ class RestfulTestCase(test_content_types.RestfulTestCase):
sql_util.teardown_test_database()
# need to reset the plug-ins
auth.controllers.AUTH_METHODS = {}
+ #drop the policy rules
+ CONF.reset()
+ rules.reset()
def new_ref(self):
"""Populates a ref with attributes common to all API entities."""
diff --git a/tests/test_v3_trust.py b/tests/test_v3_trust.py
new file mode 100644
index 00000000..19d645d8
--- /dev/null
+++ b/tests/test_v3_trust.py
@@ -0,0 +1,286 @@
+import copy
+import uuid
+import test_v3
+import json
+
+from keystone import config
+from keystone.common.sql import util as sql_util
+from keystone import test
+
+import test_content_types
+
+
+CONF = config.CONF
+DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id
+
+
+class TrustTestCase(test_v3.RestfulTestCase):
+ def setUp(self):
+ super(TrustTestCase, self).setUp()
+ self.domain = None
+ self.password = 'freeipa4all'
+ self.auth_url = '/v2.0'
+ self.admin_url = '/v2.0'
+ self.admin_url_v3 = '/v3'
+ self.url_template = "%(auth_url)s/%(resource)s"
+ self.headers = {'Content-type': 'application/json'}
+ self.trustor = self.create_user()
+ self.trustee = self.create_user()
+ self.role_1 = self.create_role()
+ self.role_2 = self.create_role()
+ self.grant_role_to_user(self.trustor['id'],
+ self.role_1['id'],
+ self.get_project()['id'])
+ self.grant_role_to_user(self.trustor['id'],
+ self.role_2['id'],
+ self.get_project()['id'])
+
+ def v3_request(self, path, data):
+ r = self.request(method='POST',
+ path=path,
+ body=data,
+ headers=self.headers)
+ return r
+
+ def get_unscoped_token_response(self, username, password):
+ url = self.url_template % {'auth_url': self.admin_url,
+ 'resource': "tokens"}
+ data = self.get_unscoped_auth(username=username, password=password)
+ r = self.restful_request(method='POST',
+ port=self._public_port(),
+ path=url,
+ body=data,
+ headers=self.headers)
+ if 'access' in r.body:
+ return r.body['access']
+ raise Exception(r)
+
+ def get_scoped_token_response(self, username, password, project_name):
+ url = self.url_template % {'auth_url': self.admin_url,
+ 'resource': "tokens"}
+ data = self.get_scoped_auth(username, password, project_name)
+ r = self.restful_request(method='POST',
+ port=self._public_port(),
+ path=url,
+ body=data,
+ headers=self.headers)
+ if 'access' in r.body:
+ return r.body['access']
+ raise Exception(r)
+
+ def get_admin_token_data(self):
+ if not hasattr(self, 'admin_token_response'):
+ self.admin_token_response = self.get_scoped_token_response(
+ 'admin', 'freeipa4all', 'demo')
+ return self.admin_token_response
+
+ def get_admin_token_id(self):
+ return 'ADMIN'
+
+ def make_admin_post_request(self, resource, data):
+ return self.make_post_request(resource,
+ data,
+ self.get_admin_token_id())
+
+ def make_post_request(self, resource, data, token_id):
+ headers = copy.copy(self.headers)
+ headers["x-auth-token"] = token_id
+ url = self.url_template % {'auth_url': self.admin_url_v3,
+ 'resource': resource}
+ r = self.restful_request(method='POST',
+ path=url,
+ port=self._admin_port(),
+ body=data,
+ headers=headers)
+ return r
+
+ def make_v2_post_request(self, resource, data, token_id):
+ headers = copy.copy(self.headers)
+ headers["x-auth-token"] = token_id
+ url = self.url_template % {'auth_url': self.admin_url,
+ 'resource': resource}
+ r = self.restful_request(method='POST',
+ path=url,
+ port=self._admin_port(),
+ body=data,
+ headers=headers)
+ return r
+
+ def make_put_request(self, resource, data, token_id):
+ headers = copy.copy(self.headers)
+ headers["x-auth-token"] = self.get_admin_token_id()
+ url = self.url_template % {'auth_url': self.admin_url_v3,
+ 'resource': resource}
+ r = self.request(method='PUT',
+ path=url,
+ port=self._admin_port(),
+ body=json.dumps(data),
+ headers=headers)
+ return r
+
+ def create_domain(self):
+ domain = self.new_domain_ref()
+ resource = 'domains'
+ data = {'domain': domain}
+ r = self.make_admin_post_request(resource, data)
+ dom = r.body['domain']
+ self.domain = dom
+
+ def create_project(self):
+ project = self.new_project_ref(
+ domain_id=self.get_domain()['id'])
+ data = {'project': project}
+ r = self.make_admin_post_request('projects', data)
+ self.project = r.body['project']
+
+ def get_domain(self):
+ if not self.domain:
+ #once authenticate supports domains, use the following function
+# self.create_domain()
+ self.domain = {'id': DEFAULT_DOMAIN_ID}
+ return self.domain
+
+ def get_project(self):
+ if not hasattr(self, 'project'):
+ self.create_project()
+ return self.project
+
+ def create_user(self):
+ user_id = uuid.uuid4().hex
+ user = {'user': {'name': uuid.uuid4().hex,
+ 'password': self.password,
+ 'enabled': True,
+ 'domain_id': self.get_domain()['id'],
+ 'project_id': self.get_project()['id']}}
+ r = self.make_admin_post_request('users', user)
+ return r.body['user']
+
+ def create_role(self):
+ ref = self.new_role_ref()
+ body = {'role': ref}
+ r = self.make_admin_post_request('roles', body)
+ return r.body['role']
+
+ def grant_role_to_user(self, user_id, role_id, project_id):
+ """PUT /projects/{project_id}/users/{user_id}/roles/{role_id}"""
+ url_template = 'projects/%(project_id)s/users'\
+ '/%(user_id)s/roles/%(role_id)s'
+ url = url_template % {'project_id': project_id,
+ 'user_id': user_id,
+ 'role_id': role_id}
+ r = self.make_put_request(url, '', self.get_admin_token_id())
+ return r
+
+ def get_scoped_auth(self, username, password, project_name):
+ return {"auth":
+ {"passwordCredentials": {"username": username,
+ "password": password},
+ "projectName": project_name}}
+
+ def get_unscoped_auth(self, username, password):
+ return {"auth":
+ {"passwordCredentials": {"username": username,
+ "password": password}}}
+
+ def create_trust(self, impersonation=True):
+ trustor_token = self.get_scoped_token_response(
+ self.trustor['name'],
+ self.password,
+ self.get_project()['name'])
+ trustee_token = self.get_unscoped_token_response(self.trustee['name'],
+ self.password)
+ trust_request = {'trust':
+ {'trustor_user_id': self.trustor['id'],
+ 'trustee_user_id': self.trustee['id'],
+ 'project_id': self.get_project()['id'],
+ 'impersonation': impersonation,
+ 'description': 'described',
+ 'roles': []}}
+ trust_response = self.make_post_request('trusts', trust_request,
+ trustor_token['token']['id'])
+ return trust_response, trustee_token
+
+ def test_create_trust(self):
+ trust_response, trustee_token = self.create_trust()
+ trust_id = trust_response.body['trust']['id']
+ self.assertEquals(trust_response.body['trust']['description'],
+ 'described')
+ auth_data = {"auth": {"token": {'id': trustee_token['token']['id']},
+ "trust_id": trust_id}}
+ r = self.make_v2_post_request("tokens",
+ auth_data,
+ trustee_token['token']['id'])
+ trust_token = r.body
+ self.assertIsNotNone(trust_token['access']['token']['id'])
+ self.assertEquals(trust_token['access']['trust']['trustee_user_id'],
+ self.trustee['id'])
+ self.assertEquals(trust_token['access']['trust']['id'], trust_id)
+
+ def test_delete_trust(self):
+ trust_response, trustee_token = self.create_trust()
+ url = self.url_template % {'auth_url': self.admin_url_v3,
+ 'resource': "trusts/"}
+ url += trust_response.body['trust']['id']
+ trustor_token = self.get_scoped_token_response(
+ self.trustor['name'],
+ self.password,
+ self.get_project()['name'])
+
+ headers = copy.copy(self.headers)
+ headers["x-auth-token"] = trustor_token['token']['id']
+ response = self.request(method='DELETE',
+ path=url,
+ port=self._public_port(),
+ body="",
+ headers=headers)
+ self.assertIsNotNone(response)
+
+ def test_list_trusts(self):
+ trustor_token = self.get_scoped_token_response(
+ self.trustor['name'],
+ self.password,
+ self.get_project()['name'])
+
+ for i in range(0, 3):
+ trust_response, trustee_token = self.create_trust()
+ url = self.url_template % {'auth_url': self.admin_url_v3,
+ 'resource': "trusts"}
+ headers = copy.copy(self.headers)
+ headers["x-auth-token"] = self.get_admin_token_id()
+ trust_lists_response = self.restful_request(method='GET',
+ path=url,
+ port=self._public_port(),
+ body="",
+ headers=headers)
+ trusts = trust_lists_response.body['trusts']
+ self.assertEqual(len(trusts), 3)
+
+ trustee_url = url + "?trustee_user_id=" + self.trustee['id']
+ headers["x-auth-token"] = trustee_token['token']['id']
+ trust_lists_response = self.restful_request(
+ method='GET', path=trustee_url, port=self._public_port(),
+ body="", headers=headers)
+ trusts = trust_lists_response.body['trusts']
+ self.assertEqual(len(trusts), 3)
+
+ headers["x-auth-token"] = trustor_token['token']['id']
+
+ trust_lists_response = self.restful_request(
+ method='GET', path=trustee_url, port=self._public_port(),
+ body="", headers=headers, expected_status=403)
+
+ trustor_url = url + "?trustor_user_id=" + self.trustor['id']
+ headers["x-auth-token"] = trustor_token['token']['id']
+ trust_lists_response = self.restful_request(
+ method='GET',
+ path=trustor_url,
+ port=self._public_port(),
+ body="",
+ headers=headers)
+ trusts = trust_lists_response.body['trusts']
+ self.assertEqual(len(trusts), 3)
+
+ headers["x-auth-token"] = trustee_token['token']['id']
+ trust_lists_response = self.restful_request(
+ method='GET', path=trustor_url, port=self._public_port(),
+ body="", headers=headers, expected_status=403)