Convert /v3/users to flask native dispatching

Convert /v3/users to use flask native dispatching.

The following test changes were required:

* Application Credentials did not have the plural form
  in the JSON Home document. The JSON Home document was
  corrected both in code and in tests.

* Application Credentials "patch" test needed to be
  refactored to look for METHOD_NOT_ALLOWED instead
  of NOT FOUND for invalid/unimplemented methods.
  The "assertValidErrorResponse" method was
  insufficient and the test now uses the flask
  test_client mechanism instead.

Change-Id: Iedaf405d11450b11e2d1fcdfae45ccb8eeb6f255
Partial-Bug: #1776504
This commit is contained in:
Morgan Fainberg 2018-10-08 14:40:56 -07:00
parent f872a40290
commit 86f968163e
21 changed files with 769 additions and 850 deletions

View File

@ -32,6 +32,7 @@ from keystone.api import roles
from keystone.api import services
from keystone.api import system
from keystone.api import trusts
from keystone.api import users
__all__ = (
'auth',
@ -56,6 +57,7 @@ __all__ = (
'services',
'system',
'trusts',
'users',
)
__apis__ = (
@ -81,4 +83,5 @@ __apis__ = (
services,
system,
trusts,
users,
)

View File

@ -19,6 +19,11 @@ import functools
from keystone.common import json_home
# OS-EC2 "extension"
os_ec2_resource_rel_func = functools.partial(
json_home.build_v3_extension_resource_relation,
extension_name='OS-EC2', extension_version='1.0')
# OS-EP-FILTER "extension"
os_ep_filter_resource_rel_func = functools.partial(
json_home.build_v3_extension_resource_relation,

723
keystone/api/users.py Normal file
View File

@ -0,0 +1,723 @@
# 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.
# This file handles all flask-restful resources for /v3/users
import base64
import os
import uuid
import flask
from oslo_log import log
from oslo_serialization import jsonutils
from six.moves import http_client
from werkzeug import exceptions
from keystone.api._shared import json_home_relations
from keystone.application_credential import schema as app_cred_schema
from keystone.common import json_home
from keystone.common import provider_api
from keystone.common import rbac_enforcer
from keystone.common import utils
from keystone.common import validation
import keystone.conf
from keystone import exception as ks_exception
from keystone.i18n import _
from keystone.identity import schema
from keystone import notifications
from keystone.server import flask as ks_flask
CRED_TYPE_EC2 = 'ec2'
CONF = keystone.conf.CONF
ENFORCER = rbac_enforcer.RBACEnforcer
LOG = log.getLogger(__name__)
PROVIDERS = provider_api.ProviderAPIs
ACCESS_TOKEN_ID_PARAMETER_RELATION = (
json_home_relations.os_oauth1_parameter_rel_func(
parameter_name='access_token_id')
)
def _convert_v3_to_ec2_credential(credential):
# Prior to bug #1259584 fix, blob was stored unserialized
# but it should be stored as a json string for compatibility
# with the v3 credentials API. Fall back to the old behavior
# for backwards compatibility with existing DB contents
try:
blob = jsonutils.loads(credential['blob'])
except TypeError:
blob = credential['blob']
return {'user_id': credential.get('user_id'),
'tenant_id': credential.get('project_id'),
'access': blob.get('access'),
'secret': blob.get('secret'),
'trust_id': blob.get('trust_id')}
def _format_token_entity(entity):
formatted_entity = entity.copy()
access_token_id = formatted_entity['id']
user_id = formatted_entity.get('authorizing_user_id', '')
if 'role_ids' in entity:
formatted_entity.pop('role_ids')
if 'access_secret' in entity:
formatted_entity.pop('access_secret')
url = ('/users/%(user_id)s/OS-OAUTH1/access_tokens/%(access_token_id)s'
'/roles' % {'user_id': user_id,
'access_token_id': access_token_id})
formatted_entity.setdefault('links', {})
formatted_entity['links']['roles'] = (ks_flask.base_url(url))
return formatted_entity
def _check_unrestricted_application_credential(token):
if 'application_credential' in token.methods:
if not token.application_credential['unrestricted']:
action = _("Using method 'application_credential' is not "
"allowed for managing additional application "
"credentials.")
raise ks_exception.ForbiddenAction(action=action)
def _build_enforcer_target_data_owner_and_user_id_match():
ref = {}
if flask.request.view_args:
credential_id = flask.request.view_args.get('credential_id')
if credential_id is not None:
hashed_id = utils.hash_access_key(credential_id)
ref['credential'] = PROVIDERS.credential_api.get_credential(
hashed_id)
return ref
def _format_role_entity(role_id):
role = PROVIDERS.role_api.get_role(role_id)
formatted_entity = role.copy()
if 'description' in role:
formatted_entity.pop('description')
if 'enabled' in role:
formatted_entity.pop('enabled')
return formatted_entity
class UserResource(ks_flask.ResourceBase):
collection_key = 'users'
member_key = 'user'
get_member_from_driver = PROVIDERS.deferred_provider_lookup(
api='identity_api', method='get_user')
def get(self, user_id=None):
"""Get a user resource or list users.
GET/HEAD /v3/users
GET/HEAD /v3/users/{user_id}
"""
if user_id is not None:
return self._get_user(user_id)
return self._list_users()
def _get_user(self, user_id):
"""Get a user resource.
GET/HEAD /v3/users/{user_id}
"""
ENFORCER.enforce_call(action='identity:get_user')
ref = PROVIDERS.identity_api.get_user(user_id)
return self.wrap_member(ref)
def _list_users(self):
"""List users.
GET/HEAD /v3/users
"""
filters = ('domain_id', 'enabled', 'idp_id', 'name', 'protocol_id',
'unique_id', 'password_expires_at')
hints = self.build_driver_hints(filters)
ENFORCER.enforce_call(action='identity:list_users', filters=filters)
domain = self._get_domain_id_for_list_request()
refs = PROVIDERS.identity_api.list_users(
domain_scope=domain, hints=hints)
return self.wrap_collection(refs, hints=hints)
def post(self):
"""Create a user.
POST /v3/users
"""
ENFORCER.enforce_call(action='identity:create_user')
user_data = self.request_body_json.get('user', {})
validation.lazy_validate(schema.user_create, user_data)
user_data = self._normalize_dict(user_data)
user_data = self._normalize_domain_id(user_data)
ref = PROVIDERS.identity_api.create_user(
user_data,
initiator=self.audit_initiator)
return self.wrap_member(ref), http_client.CREATED
def patch(self, user_id):
"""Update a user.
PATCH /v3/users/{user_id}
"""
ENFORCER.enforce_call(action='identity:update_user')
user_data = self.request_body_json.get('user', {})
validation.lazy_validate(schema.user_update, user_data)
self._require_matching_id(user_data)
ref = PROVIDERS.identity_api.update_user(
user_id, user_data, initiator=self.audit_initiator)
return self.wrap_member(ref)
def delete(self, user_id):
"""Delete a user.
DELETE /v3/users/{user_id}
"""
ENFORCER.enforce_call(action='identity:delete_user')
PROVIDERS.identity_api.delete_user(user_id)
return None, http_client.NO_CONTENT
class UserChangePasswordResource(ks_flask.ResourceBase):
collection_key = '__UNUSED__'
member_key = '__UNUSED__'
@ks_flask.unenforced_api
def get(self, user_id):
# Special case, GET is not allowed.
raise exceptions.MethodNotAllowed(valid_methods=['POST'])
@ks_flask.unenforced_api
def post(self, user_id):
user_data = self.request_body_json.get('user', {})
original_password = user_data.get('original_password')
new_password = user_data.get('password')
# TODO(morgan): Convert this to JSON Schema validation
if original_password is None:
raise ks_exception.ValidationError(
target='user',
attribute='original_password')
# TODO(morgan): Convert this to JSON Schema validation
if new_password is None:
raise ks_exception.ValidationError(
target='user',
attribute='password')
try:
PROVIDERS.identity_api.change_password(
user_id=user_id,
original_password=original_password,
new_password=new_password,
initiator=self.audit_initiator)
except AssertionError as e:
raise ks_exception.Unauthorized(
_('Error when changing user password: %s') % e
)
return None, http_client.NO_CONTENT
class UserProjectsResource(ks_flask.ResourceBase):
collection_key = 'projects'
member_key = 'project'
get_member_from_driver = PROVIDERS.deferred_provider_lookup(
api='resource_api', method='get_project')
def get(self, user_id):
filters = ('domain_id', 'enabled', 'name')
ENFORCER.enforce_call(action='identity:list_user_projects',
filters=filters)
hints = self.build_driver_hints(filters)
refs = PROVIDERS.assignment_api.list_projects_for_user(user_id)
return self.wrap_collection(refs, hints=hints)
class UserGroupsResource(ks_flask.ResourceBase):
collection_key = 'groups'
member_key = 'group'
get_member_from_driver = PROVIDERS.deferred_provider_lookup(
api='identity_api', method='get_group')
@staticmethod
def _built_target_attr_enforcement():
ref = {}
if flask.request.view_args:
ref['user'] = PROVIDERS.identity_api.get_user(
flask.request.view_args.get('user_id'))
return ref
def get(self, user_id):
"""Get groups for a user.
GET/HEAD /v3/users/{user_id}/groups
"""
filters = ('name',)
hints = self.build_driver_hints(filters)
ENFORCER.enforce_call(action='identity:list_groups_for_user',
build_target=self._built_target_attr_enforcement,
filters=filters)
refs = PROVIDERS.identity_api.list_groups_for_user(user_id=user_id,
hints=hints)
return self.wrap_collection(refs, hints=hints)
class _UserOSEC2CredBaseResource(ks_flask.ResourceBase):
collection_key = 'credentials'
member_key = 'credential'
@classmethod
def _add_self_referential_link(cls, ref, collection_name=None):
# NOTE(morgan): This should be refactored to have an EC2 Cred API with
# a sane prefix instead of overloading the "_add_self_referential_link"
# method. This was chosen as it more closely mirrors the pre-flask
# code (for transition).
path = '/users/%(user_id)s/credentials/OS-EC2/%(credential_id)s'
url = ks_flask.base_url(path) % {
'user_id': ref['user_id'],
'credential_id': ref['access']}
ref.setdefault('links', {})
ref['links']['self'] = url
class UserOSEC2CredentialsResourceListCreate(_UserOSEC2CredBaseResource):
def get(self, user_id):
"""List EC2 Credentials for user.
GET/HEAD /v3/users/{user_id}/credentials/OS-EC2
"""
ENFORCER.enforce_call(action='identity:ec2_list_credentials')
PROVIDERS.identity_api.get_user(user_id)
credential_refs = PROVIDERS.credential_api.list_credentials_for_user(
user_id, type=CRED_TYPE_EC2)
collection_refs = [
_convert_v3_to_ec2_credential(cred)
for cred in credential_refs
]
return self.wrap_collection(collection_refs)
def post(self, user_id):
"""Create EC2 Credential for user.
POST /v3/users/{user_id}/credentials/OS-EC2
"""
ENFORCER.enforce_call(action='identity:ec2_create_credential')
PROVIDERS.identity_api.get_user(user_id)
tenant_id = self.request_body_json.get('tenant_id')
PROVIDERS.resource_api.get_project(tenant_id)
blob = dict(
access=uuid.uuid4().hex,
secret=uuid.uuid4().hex,
trust_id=self.oslo_context.trust_id
)
credential_id = utils.hash_access_key(blob['access'])
cred_data = dict(
user_id=user_id,
project_id=tenant_id,
blob=jsonutils.dumps(blob),
id=credential_id,
type=CRED_TYPE_EC2
)
PROVIDERS.credential_api.create_credential(credential_id, cred_data)
ref = _convert_v3_to_ec2_credential(cred_data)
return self.wrap_member(ref), http_client.CREATED
class UserOSEC2CredentialsResourceGetDelete(_UserOSEC2CredBaseResource):
@staticmethod
def _get_cred_data(credential_id):
cred = PROVIDERS.credential_api.get_credential(credential_id)
if not cred or cred['type'] != CRED_TYPE_EC2:
raise ks_exception.Unauthorized(
message=_('EC2 access key not found.'))
return _convert_v3_to_ec2_credential(cred)
def get(self, user_id, credential_id):
"""Get a specific EC2 credential.
GET/HEAD /users/{user_id}/credentials/OS-EC2/{credential_id}
"""
func = _build_enforcer_target_data_owner_and_user_id_match
ENFORCER.enforce_call(
action='identity:ec2_get_credential',
build_target=func)
PROVIDERS.identity_api.get_user(user_id)
ec2_cred_id = utils.hash_access_key(credential_id)
cred_data = self._get_cred_data(ec2_cred_id)
return self.wrap_member(cred_data)
def delete(self, user_id, credential_id):
"""Delete a specific EC2 credential.
DELETE /users/{user_id}/credentials/OS-EC2/{credential_id}
"""
func = _build_enforcer_target_data_owner_and_user_id_match
ENFORCER.enforce_call(action='identity:ec2_delete_credential',
build_target=func)
PROVIDERS.identity_api.get_user(user_id)
ec2_cred_id = utils.hash_access_key(credential_id)
self._get_cred_data(ec2_cred_id)
PROVIDERS.credential_api.delete_credential(ec2_cred_id)
return None, http_client.NO_CONTENT
class _OAuth1ResourceBase(ks_flask.ResourceBase):
collection_key = 'access_tokens'
member_key = 'access_token'
@classmethod
def _add_self_referential_link(cls, ref, collection_name=None):
# NOTE(morgan): This should be refactored to have an OAuth1 API with
# a sane prefix instead of overloading the "_add_self_referential_link"
# method. This was chosen as it more closely mirrors the pre-flask
# code (for transition).
ref.setdefault('links', {})
path = '/users/%(user_id)s/OS-OAUTH1/access_tokens' % {
'user_id': ref.get('authorizing_user_id', '')
}
ref['links']['self'] = ks_flask.base_url(path) + '/' + ref['id']
class OAuth1ListAccessTokensResource(_OAuth1ResourceBase):
def get(self, user_id):
"""List OAuth1 Access Tokens for user.
GET /v3/users/{user_id}/OS=OAUTH1/access_tokens
"""
ENFORCER.enforce_call(action='identity:list_access_tokens')
if self.oslo_context.is_delegated_auth:
raise ks_exception.Forbidden(
_('Cannot list request tokens with a token '
'issued via delegation.'))
refs = PROVIDERS.oauth_api.list_access_tokens(user_id)
formatted_refs = ([_format_token_entity(x) for x in refs])
return self.wrap_collection(formatted_refs)
class OAuth1AccessTokenCRUDResource(_OAuth1ResourceBase):
def get(self, user_id, access_token_id):
"""Get specific access token.
GET/HEAD /v3/users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id}
"""
ENFORCER.enforce_call(action='identity:get_access_token')
access_token = PROVIDERS.oauth_api.get_access_token(access_token_id)
if access_token['authorizing_user_id'] != user_id:
raise ks_exception.NotFound()
access_token = _format_token_entity(access_token)
return self.wrap_member(access_token)
def delete(self, user_id, access_token_id):
"""Delete specific access token.
DELETE /v3/users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id}
"""
ENFORCER.enforce_call(
action='identity:ec2_delete_credential',
build_target=_build_enforcer_target_data_owner_and_user_id_match)
access_token = PROVIDERS.oauth_api.get_access_token(access_token_id)
reason = (
'Invalidating the token cache because an access token for '
'consumer %(consumer_id)s has been deleted. Authorization for '
'users with OAuth tokens will be recalculated and enforced '
'accordingly the next time they authenticate or validate a '
'token.' % {'consumer_id': access_token['consumer_id']}
)
notifications.invalidate_token_cache_notification(reason)
PROVIDERS.oauth_api.delete_access_token(
user_id, access_token_id, initiator=self.audit_initiator)
return None, http_client.NO_CONTENT
class OAuth1AccessTokenRoleListResource(ks_flask.ResourceBase):
collection_key = 'roles'
member_key = 'role'
def get(self, user_id, access_token_id):
"""List roles for a user access token.
GET/HEAD /v3/users/{user_id}/OS-OAUTH1/access_tokens/
{access_token_id}/roles
"""
ENFORCER.enforce_call(action='identity:list_access_token_roles')
access_token = PROVIDERS.oauth_api.get_access_token(access_token_id)
if access_token['authorizing_user_id'] != user_id:
raise ks_exception.NotFound()
authed_role_ids = access_token['role_ids']
authed_role_ids = jsonutils.loads(authed_role_ids)
refs = ([_format_role_entity(x) for x in authed_role_ids])
return self.wrap_collection(refs)
class OAuth1AccessTokenRoleResource(ks_flask.ResourceBase):
collection_key = 'roles'
member_key = 'role'
def get(self, user_id, access_token_id, role_id):
"""Get role for access token.
GET/HEAD /v3/users/{user_id}/OS-OAUTH1/access_tokens/
{access_token_id}/roles/{role_id}
"""
ENFORCER.enforce_call(action='identity:get_access_token_role')
access_token = PROVIDERS.oauth_api.get_access_token(access_token_id)
if access_token['authorizing_user_id'] != user_id:
raise ks_exception.Unauthorized(_('User IDs do not match'))
authed_role_ids = access_token['role_ids']
authed_role_ids = jsonutils.loads(authed_role_ids)
for authed_role_id in authed_role_ids:
if authed_role_id == role_id:
role = _format_role_entity(role_id)
return self.wrap_member(role)
raise ks_exception.RoleNotFound(role_id=role_id)
class UserAppCredListCreateResource(ks_flask.ResourceBase):
collection_key = 'application_credentials'
member_key = 'application_credential'
_public_parameters = frozenset([
'id',
'name',
'description',
'expires_at',
'project_id',
'roles',
# secret is only exposed after create, it is not stored
'secret',
'links',
'unrestricted'
])
@staticmethod
def _generate_secret():
length = 64
secret = os.urandom(length)
secret = base64.urlsafe_b64encode(secret)
secret = secret.rstrip(b'=')
secret = secret.decode('utf-8')
return secret
@staticmethod
def _normalize_role_list(app_cred_roles):
roles = []
for role in app_cred_roles:
if role.get('id'):
roles.append(role)
else:
roles.append(PROVIDERS.role_api.get_unique_role_by_name(
role['name']))
return roles
def get(self, user_id):
"""List application credentials for user.
GET/HEAD /v3/users/{user_id}/application_credentials
"""
filters = ('name',)
ENFORCER.enforce_call(action='identity:list_application_credentials',
filters=filters)
app_cred_api = PROVIDERS.application_credential_api
hints = self.build_driver_hints(filters)
refs = app_cred_api.list_application_credentials(user_id, hints=hints)
return self.wrap_collection(refs, hints=hints)
def post(self, user_id):
"""Create application credential.
POST /v3/users/{user_id}/application_credentials
"""
ENFORCER.enforce_call(action='identity:create_application_credential')
app_cred_data = self.request_body_json.get(
'application_credential', {})
validation.lazy_validate(app_cred_schema.application_credential_create,
app_cred_data)
token = self.auth_context['token']
_check_unrestricted_application_credential(token)
if self.oslo_context.user_id != user_id:
action = _('Cannot create an application credential for another '
'user.')
raise ks_exception.ForbiddenAction(action=action)
project_id = self.oslo_context.project_id
app_cred_data = self._assign_unique_id(app_cred_data)
if not app_cred_data.get('secret'):
app_cred_data['secret'] = self._generate_secret()
app_cred_data['user_id'] = user_id
app_cred_data['project_id'] = project_id
app_cred_data['roles'] = self._normalize_role_list(
app_cred_data.get('roles', token.roles))
if app_cred_data.get('expires_at'):
app_cred_data['expires_at'] = utils.parse_expiration_date(
app_cred_data['expires_at'])
app_cred_data = self._normalize_dict(app_cred_data)
app_cred_api = PROVIDERS.application_credential_api
try:
ref = app_cred_api.create_application_credential(
app_cred_data, initiator=self.audit_initiator)
except ks_exception.RoleAssignmentNotFound as e:
# Raise a Bad Request, not a Not Found, in accordance with the
# API-SIG recommendations:
# https://specs.openstack.org/openstack/api-wg/guidelines/http.html#failure-code-clarifications
raise ks_exception.ApplicationCredentialValidationError(
detail=str(e))
return self.wrap_member(ref), http_client.CREATED
class UserAppCredGetDeleteResource(ks_flask.ResourceBase):
collection_key = 'application_credentials'
member_key = 'application_credential'
def get(self, user_id, application_credential_id):
"""Get application credential resource.
GET/HEAD /v3/users/{user_id}/application_credentials/
{application_credential_id}
"""
ENFORCER.enforce_call(action='identity:get_application_credential')
ref = PROVIDERS.application_credential_api.get_application_credential(
application_credential_id)
return self.wrap_member(ref)
def delete(self, user_id, application_credential_id):
"""Delete application credential resource.
DELETE /v3/users/{user_id}/application_credentials/
{application_credential_id}
"""
ENFORCER.enforce_call(action='identity:delete_application_credential')
token = self.auth_context['token']
_check_unrestricted_application_credential(token)
PROVIDERS.application_credential_api.delete_application_credential(
application_credential_id, initiator=self.audit_initiator)
return None, http_client.NO_CONTENT
class UserAPI(ks_flask.APIBase):
_name = 'users'
_import_name = __name__
resources = [UserResource]
resource_mapping = [
ks_flask.construct_resource_map(
resource=UserChangePasswordResource,
url='/users/<string:user_id>/password',
resource_kwargs={},
rel='user_change_password',
path_vars={'user_id': json_home.Parameters.USER_ID}
),
ks_flask.construct_resource_map(
resource=UserGroupsResource,
url='/users/<string:user_id>/groups',
resource_kwargs={},
rel='user_groups',
path_vars={'user_id': json_home.Parameters.USER_ID}
),
ks_flask.construct_resource_map(
resource=UserProjectsResource,
url='/users/<string:user_id>/projects',
resource_kwargs={},
rel='user_projects',
path_vars={'user_id': json_home.Parameters.USER_ID}
),
ks_flask.construct_resource_map(
resource=UserOSEC2CredentialsResourceListCreate,
url='/users/<string:user_id>/credentials/OS-EC2',
resource_kwargs={},
rel='user_credentials',
resource_relation_func=(
json_home_relations.os_ec2_resource_rel_func),
path_vars={'user_id': json_home.Parameters.USER_ID}
),
ks_flask.construct_resource_map(
resource=UserOSEC2CredentialsResourceGetDelete,
url=('/users/<string:user_id>/credentials/OS-EC2/'
'<string:credential_id>'),
resource_kwargs={},
rel='user_credential',
resource_relation_func=(
json_home_relations.os_ec2_resource_rel_func),
path_vars={
'credential_id': json_home.build_v3_parameter_relation(
'credential_id'),
'user_id': json_home.Parameters.USER_ID}
),
ks_flask.construct_resource_map(
resource=OAuth1ListAccessTokensResource,
url='/users/<string:user_id>/OS-OAUTH1/access_tokens',
resource_kwargs={},
rel='user_access_tokens',
resource_relation_func=(
json_home_relations.os_oauth1_resource_rel_func),
path_vars={'user_id': json_home.Parameters.USER_ID}
),
ks_flask.construct_resource_map(
resource=OAuth1AccessTokenCRUDResource,
url=('/users/<string:user_id>/OS-OAUTH1/'
'access_tokens/<string:access_token_id>'),
resource_kwargs={},
rel='user_access_token',
resource_relation_func=(
json_home_relations.os_oauth1_resource_rel_func),
path_vars={
'access_token_id': ACCESS_TOKEN_ID_PARAMETER_RELATION,
'user_id': json_home.Parameters.USER_ID}
),
ks_flask.construct_resource_map(
resource=OAuth1AccessTokenRoleListResource,
url=('/users/<string:user_id>/OS-OAUTH1/access_tokens/'
'<string:access_token_id>/roles'),
resource_kwargs={},
rel='user_access_token_roles',
resource_relation_func=(
json_home_relations.os_oauth1_resource_rel_func),
path_vars={'access_token_id': ACCESS_TOKEN_ID_PARAMETER_RELATION,
'user_id': json_home.Parameters.USER_ID}
),
ks_flask.construct_resource_map(
resource=OAuth1AccessTokenRoleResource,
url=('/users/<string:user_id>/OS-OAUTH1/access_tokens/'
'<string:access_token_id>/roles/<string:role_id>'),
resource_kwargs={},
rel='user_access_token_role',
resource_relation_func=(
json_home_relations.os_oauth1_resource_rel_func),
path_vars={'access_token_id': ACCESS_TOKEN_ID_PARAMETER_RELATION,
'role_id': json_home.Parameters.ROLE_ID,
'user_id': json_home.Parameters.USER_ID}
),
ks_flask.construct_resource_map(
resource=UserAppCredListCreateResource,
url='/users/<string:user_id>/application_credentials',
resource_kwargs={},
rel='application_credentials',
path_vars={'user_id': json_home.Parameters.USER_ID}
),
ks_flask.construct_resource_map(
resource=UserAppCredGetDeleteResource,
url=('/users/<string:user_id>/application_credentials/'
'<string:application_credential_id>'),
resource_kwargs={},
rel='application_credential',
path_vars={
'user_id': json_home.Parameters.USER_ID,
'application_credential_id':
json_home.Parameters.APPLICATION_CRED_ID}
)
]
APIs = (UserAPI,)

View File

@ -10,5 +10,4 @@
# License for the specific language governing permissions and limitations
# under the License.
from keystone.application_credential import controllers # noqa
from keystone.application_credential.core import * # noqa

View File

@ -1,153 +0,0 @@
# Copyright 2018 SUSE Linux GmbH
#
# 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.
"""Workflow Logic the Application Credential service."""
import base64
import os
from oslo_log import log
from keystone.application_credential import schema
from keystone.common import controller
from keystone.common import provider_api
from keystone.common import utils
from keystone.common import validation
import keystone.conf
from keystone import exception
from keystone.i18n import _
CONF = keystone.conf.CONF
LOG = log.getLogger(__name__)
PROVIDERS = provider_api.ProviderAPIs
class ApplicationCredentialV3(controller.V3Controller):
collection_name = 'application_credentials'
member_name = 'application_credential'
_public_parameters = frozenset([
'id',
'name',
'description',
'expires_at',
'project_id',
'roles',
# secret is only exposed after create, it is not stored
'secret',
'links',
'unrestricted'
])
def _normalize_role_list(self, app_cred_roles):
roles = []
for role in app_cred_roles:
if role.get('id'):
roles.append(role)
else:
roles.append(PROVIDERS.role_api.get_unique_role_by_name(
role['name']))
return roles
def _generate_secret(self):
length = 64
secret = os.urandom(length)
secret = base64.urlsafe_b64encode(secret)
secret = secret.rstrip(b'=')
secret = secret.decode('utf-8')
return secret
@classmethod
def _add_self_referential_link(cls, context, ref):
path = ('/users/%(user_id)s/application_credentials') % {
'user_id': ref['user_id']}
ref.setdefault('links', {})
ref['links']['self'] = cls.base_url(
context, path=path) + '/' + ref['id']
return ref
@classmethod
def wrap_member(cls, context, ref):
cls._add_self_referential_link(context, ref)
ref = cls.filter_params(ref)
return {cls.member_name: ref}
def _check_unrestricted(self, token):
if 'application_credential' in token.methods:
if not token.application_credential['unrestricted']:
action = _("Using method 'application_credential' is not "
"allowed for managing additional application "
"credentials.")
raise exception.ForbiddenAction(action=action)
@controller.protected()
def create_application_credential(self, request, user_id,
application_credential):
validation.lazy_validate(schema.application_credential_create,
application_credential)
token = request.auth_context['token']
self._check_unrestricted(token)
if request.context.user_id != user_id:
action = _("Cannot create an application credential for another "
"user")
raise exception.ForbiddenAction(action=action)
project_id = request.context.project_id
app_cred = self._assign_unique_id(application_credential)
if not app_cred.get('secret'):
app_cred['secret'] = self._generate_secret()
app_cred['user_id'] = user_id
app_cred['project_id'] = project_id
app_cred['roles'] = self._normalize_role_list(
app_cred.get('roles', token.roles))
if app_cred.get('expires_at'):
app_cred['expires_at'] = utils.parse_expiration_date(
app_cred['expires_at'])
app_cred = self._normalize_dict(app_cred)
app_cred_api = PROVIDERS.application_credential_api
try:
ref = app_cred_api.create_application_credential(
app_cred, initiator=request.audit_initiator
)
except exception.RoleAssignmentNotFound as e:
# Raise a Bad Request, not a Not Found, in accordance with the
# API-SIG recommendations:
# https://specs.openstack.org/openstack/api-wg/guidelines/http.html#failure-code-clarifications
raise exception.ApplicationCredentialValidationError(
detail=str(e))
return ApplicationCredentialV3.wrap_member(request.context_dict, ref)
@controller.filterprotected('name')
def list_application_credentials(self, request, filters, user_id):
app_cred_api = PROVIDERS.application_credential_api
hints = ApplicationCredentialV3.build_driver_hints(request, filters)
refs = app_cred_api.list_application_credentials(user_id, hints=hints)
return ApplicationCredentialV3.wrap_collection(request.context_dict,
refs)
@controller.protected()
def get_application_credential(self, request, user_id,
application_credential_id):
ref = PROVIDERS.application_credential_api.get_application_credential(
application_credential_id)
return ApplicationCredentialV3.wrap_member(request.context_dict, ref)
@controller.protected()
def delete_application_credential(self, request, user_id,
application_credential_id):
token = request.auth_context['token']
self._check_unrestricted(token)
PROVIDERS.application_credential_api.delete_application_credential(
application_credential_id, initiator=request.audit_initiator
)

View File

@ -1,55 +0,0 @@
# Copyright 2018 SUSE Linux GmbH
#
# 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 Application Credential service."""
from keystone.application_credential import controllers
from keystone.common import json_home
from keystone.common import wsgi
APP_CRED_RESOURCE_RELATION = json_home.build_v3_resource_relation(
'application_credential')
APP_CRED_PARAMETER_RELATION = json_home.build_v3_parameter_relation(
'application_credential_id')
APP_CRED_COLLECTION_PATH = '/users/{user_id}/application_credentials'
APP_CRED_RESOURCE_PATH = (
'/users/{user_id}/application_credentials/{application_credential_id}'
)
class Routers(wsgi.RoutersBase):
_path_prefixes = (APP_CRED_COLLECTION_PATH, 'users',)
def append_v3_routers(self, mapper, routers):
app_cred_controller = controllers.ApplicationCredentialV3()
self._add_resource(
mapper, app_cred_controller,
path=APP_CRED_COLLECTION_PATH,
get_head_action='list_application_credentials',
post_action='create_application_credential',
rel=APP_CRED_RESOURCE_RELATION,
path_vars={
'user_id': json_home.Parameters.USER_ID,
})
self._add_resource(
mapper, app_cred_controller,
path=APP_CRED_RESOURCE_PATH,
get_head_action='get_application_credential',
delete_action='delete_application_credential',
rel=APP_CRED_RESOURCE_RELATION,
path_vars={
'user_id': json_home.Parameters.USER_ID,
'application_credential_id': APP_CRED_PARAMETER_RELATION,
})

View File

@ -31,25 +31,6 @@ LOG = log.getLogger(__name__)
PROVIDERS = provider_api.ProviderAPIs
class ProjectAssignmentV3(controller.V3Controller):
"""The V3 Project APIs that are processing assignments."""
collection_name = 'projects'
member_name = 'project'
def __init__(self):
super(ProjectAssignmentV3, self).__init__()
self.get_member_from_driver = PROVIDERS.resource_api.get_project
@controller.filterprotected('domain_id', 'enabled', 'name')
def list_user_projects(self, request, filters, user_id):
hints = ProjectAssignmentV3.build_driver_hints(request, filters)
refs = PROVIDERS.assignment_api.list_projects_for_user(user_id)
return ProjectAssignmentV3.wrap_collection(request.context_dict,
refs,
hints=hints)
class GrantAssignmentV3(controller.V3Controller):
"""The V3 Grant Assignment APIs."""

View File

@ -20,31 +20,12 @@ from keystone.common import json_home
from keystone.common import wsgi
class Public(wsgi.ComposableRouter):
def add_routes(self, mapper):
tenant_controller = controllers.TenantAssignment()
mapper.connect('/tenants',
controller=tenant_controller,
action='get_projects_for_token',
conditions=dict(method=['GET']))
class Routers(wsgi.RoutersBase):
_path_prefixes = ('users', 'projects')
_path_prefixes = ('projects',)
def append_v3_routers(self, mapper, routers):
project_controller = controllers.ProjectAssignmentV3()
self._add_resource(
mapper, project_controller,
path='/users/{user_id}/projects',
get_head_action='list_user_projects',
rel=json_home.build_v3_resource_relation('user_projects'),
path_vars={
'user_id': json_home.Parameters.USER_ID,
})
grant_controller = controllers.GrantAssignmentV3()
self._add_resource(
mapper, grant_controller,

View File

@ -57,6 +57,8 @@ class Parameters(object):
TAG_VALUE = build_v3_parameter_relation('tag_value')
REGISTERED_LIMIT_ID = build_v3_parameter_relation('registered_limit_id')
LIMIT_ID = build_v3_parameter_relation('limit_id')
APPLICATION_CRED_ID = build_v3_parameter_relation(
'application_credential_id')
class Status(object):

View File

@ -34,7 +34,6 @@ Glance to list images needed to perform the requested task.
import abc
import sys
import uuid
from keystoneclient.contrib.ec2 import utils as ec2_utils
from oslo_serialization import jsonutils
@ -156,70 +155,6 @@ class Ec2ControllerCommon(provider_api.ProviderAPIMixin, object):
return user_ref, tenant_ref, roles_ref
def create_credential(self, request, user_id, tenant_id):
"""Create a secret/access pair for use with ec2 style auth.
Generates a new set of credentials that map the user/tenant
pair.
:param request: current request
:param user_id: id of user
:param tenant_id: id of tenant
:returns: credential: dict of ec2 credential
"""
self.identity_api.get_user(user_id)
self.resource_api.get_project(tenant_id)
blob = {'access': uuid.uuid4().hex,
'secret': uuid.uuid4().hex,
'trust_id': request.context.trust_id}
credential_id = utils.hash_access_key(blob['access'])
cred_ref = {'user_id': user_id,
'project_id': tenant_id,
'blob': jsonutils.dumps(blob),
'id': credential_id,
'type': CRED_TYPE_EC2}
self.credential_api.create_credential(credential_id, cred_ref)
return {'credential': self._convert_v3_to_ec2_credential(cred_ref)}
def get_credentials(self, user_id):
"""List all credentials for a user.
:param user_id: id of user
:returns: credentials: list of ec2 credential dicts
"""
self.identity_api.get_user(user_id)
credential_refs = self.credential_api.list_credentials_for_user(
user_id, type=CRED_TYPE_EC2)
return {'credentials':
[self._convert_v3_to_ec2_credential(credential)
for credential in credential_refs]}
def get_credential(self, user_id, credential_id):
"""Retrieve a user's access/secret pair by the access key.
Grab the full access/secret pair for a given access key.
:param user_id: id of user
:param credential_id: access key for credentials
:returns: credential: dict of ec2 credential
"""
self.identity_api.get_user(user_id)
return {'credential': self._get_credentials(credential_id)}
def delete_credential(self, user_id, credential_id):
"""Delete a user's access/secret pair.
Used to revoke a user's access/secret pair
:param user_id: id of user
:param credential_id: access key for credentials
:returns: bool: success
"""
self.identity_api.get_user(user_id)
self._get_credentials(credential_id)
ec2_credential_id = utils.hash_access_key(credential_id)
return self.credential_api.delete_credential(ec2_credential_id)
@staticmethod
def _convert_v3_to_ec2_credential(credential):
# Prior to bug #1259584 fix, blob was stored unserialized
@ -271,22 +206,6 @@ class Ec2ControllerV3(Ec2ControllerCommon, controller.V3Controller):
collection_name = 'credentials'
member_name = 'credential'
def _check_credential_owner_and_user_id_match(self, request, prep_info,
user_id, credential_id):
# NOTE(morganfainberg): this method needs to capture the arguments of
# the method that is decorated with @controller.protected() (with
# exception of the first argument ('context') since the protected
# method passes in *args, **kwargs. In this case, it is easier to see
# the expected input if the argspec is `user_id` and `credential_id`
# explicitly (matching the :class:`.ec2_delete_credential()` method
# below).
ref = {}
credential_id = utils.hash_access_key(credential_id)
ref['credential'] = self.credential_api.get_credential(credential_id)
# NOTE(morganfainberg): policy_api is required for this
# check_protection to properly be able to perform policy enforcement.
self.check_protection(request, prep_info, ref)
def authenticate(self, context, credentials=None, ec2Credentials=None):
(user_ref, project_ref, roles_ref) = self._authenticate(
credentials=credentials, ec2credentials=ec2Credentials
@ -299,37 +218,3 @@ class Ec2ControllerV3(Ec2ControllerCommon, controller.V3Controller):
)
token_reference = render_token.render_token_response_from_model(token)
return self.render_token_data_response(token.id, token_reference)
@controller.protected(callback=_check_credential_owner_and_user_id_match)
def ec2_get_credential(self, request, user_id, credential_id):
ref = super(Ec2ControllerV3, self).get_credential(user_id,
credential_id)
return Ec2ControllerV3.wrap_member(request.context_dict,
ref['credential'])
@controller.protected()
def ec2_list_credentials(self, request, user_id):
refs = super(Ec2ControllerV3, self).get_credentials(user_id)
return Ec2ControllerV3.wrap_collection(request.context_dict,
refs['credentials'])
@controller.protected()
def ec2_create_credential(self, request, user_id, tenant_id):
ref = super(Ec2ControllerV3, self).create_credential(
request, user_id, tenant_id)
return Ec2ControllerV3.wrap_member(request.context_dict,
ref['credential'])
@controller.protected(callback=_check_credential_owner_and_user_id_match)
def ec2_delete_credential(self, request, user_id, credential_id):
return super(Ec2ControllerV3, self).delete_credential(user_id,
credential_id)
@classmethod
def _add_self_referential_link(cls, context, ref):
path = '/users/%(user_id)s/credentials/OS-EC2/%(credential_id)s'
url = cls.base_url(context, path) % {
'user_id': ref['user_id'],
'credential_id': ref['access']}
ref.setdefault('links', {})
ref['links']['self'] = url

View File

@ -26,7 +26,7 @@ build_resource_relation = functools.partial(
class Routers(wsgi.RoutersBase):
_path_prefixes = ('ec2tokens', 'users')
_path_prefixes = ('ec2tokens',)
def append_v3_routers(self, mapper, routers):
ec2_controller = controllers.Ec2ControllerV3()
@ -36,25 +36,3 @@ class Routers(wsgi.RoutersBase):
path='/ec2tokens',
post_action='authenticate',
rel=build_resource_relation(resource_name='ec2tokens'))
# crud
self._add_resource(
mapper, ec2_controller,
path='/users/{user_id}/credentials/OS-EC2',
get_head_action='ec2_list_credentials',
post_action='ec2_create_credential',
rel=build_resource_relation(resource_name='user_credentials'),
path_vars={
'user_id': json_home.Parameters.USER_ID,
})
self._add_resource(
mapper, ec2_controller,
path='/users/{user_id}/credentials/OS-EC2/{credential_id}',
get_head_action='ec2_get_credential',
delete_action='ec2_delete_credential',
rel=build_resource_relation(resource_name='user_credential'),
path_vars={
'credential_id':
json_home.build_v3_parameter_relation('credential_id'),
'user_id': json_home.Parameters.USER_ID,
})

View File

@ -12,6 +12,5 @@
# License for the specific language governing permissions and limitations
# under the License.
from keystone.identity import controllers # noqa
from keystone.identity.core import * # noqa
from keystone.identity import generator # noqa

View File

@ -1,134 +0,0 @@
# Copyright 2012 OpenStack Foundation
#
# 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.
"""Workflow Logic the Identity service."""
from oslo_log import log
from keystone.common import controller
from keystone.common import provider_api
from keystone.common import validation
import keystone.conf
from keystone import exception
from keystone.i18n import _
from keystone.identity import schema
CONF = keystone.conf.CONF
LOG = log.getLogger(__name__)
PROVIDERS = provider_api.ProviderAPIs
class UserV3(controller.V3Controller):
collection_name = 'users'
member_name = 'user'
def __init__(self):
super(UserV3, self).__init__()
self.get_member_from_driver = PROVIDERS.identity_api.get_user
def _check_user_and_group_protection(self, request, prep_info,
user_id, group_id):
ref = {}
ref['user'] = PROVIDERS.identity_api.get_user(user_id)
ref['group'] = PROVIDERS.identity_api.get_group(group_id)
self.check_protection(request, prep_info, ref)
@controller.protected()
def create_user(self, request, user):
validation.lazy_validate(schema.user_create, user)
# The manager layer will generate the unique ID for users
ref = self._normalize_dict(user)
ref = self._normalize_domain_id(request, ref)
ref = PROVIDERS.identity_api.create_user(
ref, initiator=request.audit_initiator
)
return UserV3.wrap_member(request.context_dict, ref)
@controller.filterprotected('domain_id', 'enabled', 'idp_id', 'name',
'protocol_id', 'unique_id',
'password_expires_at')
def list_users(self, request, filters):
hints = UserV3.build_driver_hints(request, filters)
domain = self._get_domain_id_for_list_request(request)
refs = PROVIDERS.identity_api.list_users(
domain_scope=domain, hints=hints
)
return UserV3.wrap_collection(request.context_dict, refs, hints=hints)
@controller.protected()
def get_user(self, request, user_id):
ref = PROVIDERS.identity_api.get_user(user_id)
return UserV3.wrap_member(request.context_dict, ref)
def _update_user(self, request, user_id, user):
self._require_matching_id(user_id, user)
ref = PROVIDERS.identity_api.update_user(
user_id, user, initiator=request.audit_initiator
)
return UserV3.wrap_member(request.context_dict, ref)
@controller.protected()
def update_user(self, request, user_id, user):
validation.lazy_validate(schema.user_update, user)
return self._update_user(request, user_id, user)
@controller.protected()
def delete_user(self, request, user_id):
return PROVIDERS.identity_api.delete_user(
user_id, initiator=request.audit_initiator
)
# NOTE(gagehugo): We do not need this to be @protected.
# A user is already expected to know their password in order
# to change it, and can be authenticated as such.
def change_password(self, request, user_id, user):
original_password = user.get('original_password')
if original_password is None:
raise exception.ValidationError(target='user',
attribute='original_password')
password = user.get('password')
if password is None:
raise exception.ValidationError(target='user',
attribute='password')
try:
PROVIDERS.identity_api.change_password(
user_id, original_password,
password, initiator=request.audit_initiator)
except AssertionError as e:
raise exception.Unauthorized(_(
'Error when changing user password: %s') % e)
class GroupV3(controller.V3Controller):
collection_name = 'groups'
member_name = 'group'
def __init__(self):
super(GroupV3, self).__init__()
self.get_member_from_driver = PROVIDERS.identity_api.get_group
def _check_user_protection(self, request, prep_info, user_id):
ref = {}
ref['user'] = PROVIDERS.identity_api.get_user(user_id)
self.check_protection(request, prep_info, ref)
@controller.filterprotected('name', callback=_check_user_protection)
def list_groups_for_user(self, request, filters, user_id):
hints = GroupV3.build_driver_hints(request, filters)
refs = PROVIDERS.identity_api.list_groups_for_user(
user_id, hints=hints
)
return GroupV3.wrap_collection(request.context_dict, refs, hints=hints)

View File

@ -899,20 +899,10 @@ class Manager(manager.Manager):
# - select the right driver for this domain
# - clear/set domain_ids for drivers that do not support domains
# - create any ID mapping that might be required
# TODO(morgan): The split of "authenticate" and "_authenticate" is done
# until user API is converted to flask. This is to make webob and flask
# play nicely with the authenticate mechanism during self-service password
# changes. While this is in place, CADF notifications will not be emitted
# for self-service password changes indicating an auth attempt was being
# made. This is a very limited time transitional change.
@notifications.emit_event('authenticate')
def authenticate(self, user_id, password):
return self._authenticate(user_id, password)
@domains_configured
@exception_translated('assertion')
def _authenticate(self, user_id, password):
def authenticate(self, user_id, password):
domain_id, driver, entity_id = (
self._get_domain_driver_and_entity_id(user_id))
ref = driver.authenticate(entity_id, password)
@ -1390,9 +1380,7 @@ class Manager(manager.Manager):
# authenticate() will raise an AssertionError if authentication fails
try:
# TODO(morgan): When users is ported to flask, ensure this is
# mapped back to self.authenticate instead of self._authenticate.
self._authenticate(user_id, original_password)
self.authenticate(user_id, original_password)
except exception.PasswordExpired:
# If a password has expired, we want users to be able to change it
pass

View File

@ -1,51 +0,0 @@
# Copyright 2012 OpenStack Foundation
#
# 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 json_home
from keystone.common import router
from keystone.common import wsgi
from keystone.identity import controllers
class Routers(wsgi.RoutersBase):
_path_prefixes = ('users', )
def append_v3_routers(self, mapper, routers):
user_controller = controllers.UserV3()
routers.append(
router.Router(user_controller,
'users', 'user',
resource_descriptions=self.v3_resources))
self._add_resource(
mapper, user_controller,
path='/users/{user_id}/password',
post_action='change_password',
rel=json_home.build_v3_resource_relation('user_change_password'),
path_vars={
'user_id': json_home.Parameters.USER_ID,
})
group_controller = controllers.GroupV3()
self._add_resource(
mapper, group_controller,
path='/users/{user_id}/groups',
get_head_action='list_groups_for_user',
rel=json_home.build_v3_resource_relation('user_groups'),
path_vars={
'user_id': json_home.Parameters.USER_ID,
})

View File

@ -1,143 +0,0 @@
# Copyright 2013 OpenStack Foundation
#
# 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.
"""Extensions supporting OAuth1."""
from oslo_log import log
from oslo_serialization import jsonutils
from keystone.common import controller
from keystone.common import provider_api
import keystone.conf
from keystone import exception
from keystone.i18n import _
from keystone import notifications
CONF = keystone.conf.CONF
LOG = log.getLogger(__name__)
PROVIDERS = provider_api.ProviderAPIs
class AccessTokenCrudV3(controller.V3Controller):
collection_name = 'access_tokens'
member_name = 'access_token'
@classmethod
def _add_self_referential_link(cls, context, ref):
# NOTE(lwolf): overriding method to add proper path to self link
ref.setdefault('links', {})
path = '/users/%(user_id)s/OS-OAUTH1/access_tokens' % {
'user_id': cls._get_user_id(ref)
}
ref['links']['self'] = cls.base_url(context, path) + '/' + ref['id']
@controller.protected()
def get_access_token(self, request, user_id, access_token_id):
access_token = PROVIDERS.oauth_api.get_access_token(access_token_id)
if access_token['authorizing_user_id'] != user_id:
raise exception.NotFound()
access_token = self._format_token_entity(request.context_dict,
access_token)
return AccessTokenCrudV3.wrap_member(request.context_dict,
access_token)
@controller.protected()
def list_access_tokens(self, request, user_id):
if request.context.is_delegated_auth:
raise exception.Forbidden(
_('Cannot list request tokens'
' with a token issued via delegation.'))
refs = PROVIDERS.oauth_api.list_access_tokens(user_id)
formatted_refs = ([self._format_token_entity(request.context_dict, x)
for x in refs])
return AccessTokenCrudV3.wrap_collection(request.context_dict,
formatted_refs)
@controller.protected()
def delete_access_token(self, request, user_id, access_token_id):
access_token = PROVIDERS.oauth_api.get_access_token(access_token_id)
reason = (
'Invalidating the token cache because an access token for '
'consumer %(consumer_id)s has been deleted. Authorization for '
'users with OAuth tokens will be recalculated and enforced '
'accordingly the next time they authenticate or validate a '
'token.' % {'consumer_id': access_token['consumer_id']}
)
notifications.invalidate_token_cache_notification(reason)
return PROVIDERS.oauth_api.delete_access_token(
user_id, access_token_id, initiator=request.audit_initiator
)
@staticmethod
def _get_user_id(entity):
return entity.get('authorizing_user_id', '')
def _format_token_entity(self, context, entity):
formatted_entity = entity.copy()
access_token_id = formatted_entity['id']
user_id = self._get_user_id(formatted_entity)
if 'role_ids' in entity:
formatted_entity.pop('role_ids')
if 'access_secret' in entity:
formatted_entity.pop('access_secret')
url = ('/users/%(user_id)s/OS-OAUTH1/access_tokens/%(access_token_id)s'
'/roles' % {'user_id': user_id,
'access_token_id': access_token_id})
formatted_entity.setdefault('links', {})
formatted_entity['links']['roles'] = (self.base_url(context, url))
return formatted_entity
class AccessTokenRolesV3(controller.V3Controller):
collection_name = 'roles'
member_name = 'role'
@controller.protected()
def list_access_token_roles(self, request, user_id, access_token_id):
access_token = PROVIDERS.oauth_api.get_access_token(access_token_id)
if access_token['authorizing_user_id'] != user_id:
raise exception.NotFound()
authed_role_ids = access_token['role_ids']
authed_role_ids = jsonutils.loads(authed_role_ids)
refs = ([self._format_role_entity(x) for x in authed_role_ids])
return AccessTokenRolesV3.wrap_collection(request.context_dict, refs)
@controller.protected()
def get_access_token_role(self, request, user_id,
access_token_id, role_id):
access_token = PROVIDERS.oauth_api.get_access_token(access_token_id)
if access_token['authorizing_user_id'] != user_id:
raise exception.Unauthorized(_('User IDs do not match'))
authed_role_ids = access_token['role_ids']
authed_role_ids = jsonutils.loads(authed_role_ids)
for authed_role_id in authed_role_ids:
if authed_role_id == role_id:
role = self._format_role_entity(role_id)
return AccessTokenRolesV3.wrap_member(request.context_dict,
role)
raise exception.RoleNotFound(role_id=role_id)
def _format_role_entity(self, role_id):
role = PROVIDERS.role_api.get_role(role_id)
formatted_entity = role.copy()
if 'description' in role:
formatted_entity.pop('description')
if 'enabled' in role:
formatted_entity.pop('enabled')
return formatted_entity

View File

@ -1,101 +0,0 @@
# Copyright 2013 OpenStack Foundation
#
# 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 functools
from keystone.common import json_home
from keystone.common import wsgi
from keystone.oauth1 import controllers
build_resource_relation = functools.partial(
json_home.build_v3_extension_resource_relation,
extension_name='OS-OAUTH1', extension_version='1.0')
build_parameter_relation = functools.partial(
json_home.build_v3_extension_parameter_relation,
extension_name='OS-OAUTH1', extension_version='1.0')
ACCESS_TOKEN_ID_PARAMETER_RELATION = build_parameter_relation(
parameter_name='access_token_id')
class Routers(wsgi.RoutersBase):
"""API Endpoints for the OAuth1 extension.
The goal of this extension is to allow third-party service providers
to acquire tokens with a limited subset of a user's roles for acting
on behalf of that user. This is done using an oauth-similar flow and
api.
The API looks like::
# User access token crud
GET /users/{user_id}/OS-OAUTH1/access_tokens
GET /users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id}
GET /users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id}/roles
GET /users/{user_id}/OS-OAUTH1/access_tokens
/{access_token_id}/roles/{role_id}
DELETE /users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id}
"""
_path_prefixes = ('users',)
def append_v3_routers(self, mapper, routers):
access_token_controller = controllers.AccessTokenCrudV3()
access_token_roles_controller = controllers.AccessTokenRolesV3()
# user access token crud
self._add_resource(
mapper, access_token_controller,
path='/users/{user_id}/OS-OAUTH1/access_tokens',
get_head_action='list_access_tokens',
rel=build_resource_relation(resource_name='user_access_tokens'),
path_vars={
'user_id': json_home.Parameters.USER_ID,
})
self._add_resource(
mapper, access_token_controller,
path='/users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id}',
get_head_action='get_access_token',
delete_action='delete_access_token',
rel=build_resource_relation(resource_name='user_access_token'),
path_vars={
'access_token_id': ACCESS_TOKEN_ID_PARAMETER_RELATION,
'user_id': json_home.Parameters.USER_ID,
})
self._add_resource(
mapper, access_token_roles_controller,
path='/users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id}/'
'roles',
get_head_action='list_access_token_roles',
rel=build_resource_relation(
resource_name='user_access_token_roles'),
path_vars={
'access_token_id': ACCESS_TOKEN_ID_PARAMETER_RELATION,
'user_id': json_home.Parameters.USER_ID,
})
self._add_resource(
mapper, access_token_roles_controller,
path='/users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id}/'
'roles/{role_id}',
get_head_action='get_access_token_role',
rel=build_resource_relation(
resource_name='user_access_token_role'),
path_vars={
'access_token_id': ACCESS_TOKEN_ID_PARAMETER_RELATION,
'role_id': json_home.Parameters.ROLE_ID,
'user_id': json_home.Parameters.USER_ID,
})

View File

@ -24,13 +24,10 @@ import routes
import werkzeug.wsgi
import keystone.api
from keystone.application_credential import routers as app_cred_routers
from keystone.assignment import routers as assignment_routers
from keystone.common import wsgi as keystone_wsgi
from keystone.contrib.ec2 import routers as ec2_routers
from keystone.contrib.s3 import routers as s3_routers
from keystone.identity import routers as identity_routers
from keystone.oauth1 import routers as oauth1_routers
from keystone.resource import routers as resource_routers
# TODO(morgan): _MOVED_API_PREFIXES to be removed when the legacy dispatch
@ -57,6 +54,7 @@ _MOVED_API_PREFIXES = frozenset(
'roles',
'services',
'system',
'users',
]
)
@ -64,10 +62,7 @@ LOG = log.getLogger(__name__)
ALL_API_ROUTERS = [assignment_routers,
identity_routers,
app_cred_routers,
resource_routers,
oauth1_routers,
ec2_routers,
s3_routers]

View File

@ -921,7 +921,7 @@ class ResourceBase(flask_restful.Resource):
if token_ref.domain_scoped:
return token_ref.domain_id
elif token_ref.project_scoped:
return token_ref.project_domain_id
return token_ref.project_domain['id']
else:
msg = 'No domain information specified as part of list request'
tr_msg = _('No domain information specified as part of list '
@ -941,7 +941,8 @@ class ResourceBase(flask_restful.Resource):
# Retrieve the auth context that was prepared by
# AuthContextMiddleware.
auth_context = cls.auth_context
auth_context = flask.request.environ.get(
authorization.AUTH_CONTEXT_ENV, {})
return auth_context['token']
except KeyError:
LOG.warning("Couldn't find the auth context.")

View File

@ -292,15 +292,26 @@ class ApplicationCredentialTestCase(test_v3.RestfulTestCase):
expected_status=http_client.NO_CONTENT)
def test_update_application_credential(self):
roles = [{'id': self.role_id}]
app_cred_body = self._app_cred_body(roles=roles)
resp = self.post('/users/%s/application_credentials' % self.user_id,
body=app_cred_body,
expected_status=http_client.CREATED)
# Application credentials are immutable
app_cred_body['application_credential']['description'] = "New Things"
app_cred_id = resp.json['application_credential']['id']
self.patch(MEMBER_PATH_FMT % {'user_id': self.user_id,
'app_cred_id': app_cred_id},
body=app_cred_body,
expected_status=http_client.NOT_FOUND)
with self.test_client() as c:
roles = [{'id': self.role_id}]
app_cred_body = self._app_cred_body(roles=roles)
token = self.get_scoped_token()
resp = c.post(
'/v3/users/%s/application_credentials' % self.user_id,
json=app_cred_body,
expected_status_code=http_client.CREATED,
headers={'X-Auth-Token': token})
# Application credentials are immutable
app_cred_body['application_credential'][
'description'] = "New Things"
app_cred_id = resp.json['application_credential']['id']
# NOTE(morgan): when the whole test case is converted to using
# flask test_client, this extra v3 prefix will
# need to be rolled into the base MEMBER_PATH_FMT
member_path = '/v3%s' % MEMBER_PATH_FMT % {
'user_id': self.user_id,
'app_cred_id': app_cred_id}
c.patch(member_path,
json=app_cred_body,
expected_status_code=http_client.METHOD_NOT_ALLOWED,
headers={'X-Auth-Token': token})

View File

@ -142,6 +142,7 @@ FEDERATED_IDP_SPECIFIC_WEBSSO = ('/auth/OS-FEDERATION/identity_providers/'
APPLICATION_CREDENTIAL = ('/users/{user_id}/application_credentials/'
'{application_credential_id}')
APPLICATION_CREDENTIALS = '/users/{user_id}/application_credentials'
APPLICATION_CREDENTIAL_RELATION = (
json_home.build_v3_parameter_relation('application_credential_id'))
@ -633,6 +634,10 @@ V3_JSON_HOME_RESOURCES = {
'href': '/limits/model',
'hints': {'status': 'experimental'}
},
json_home.build_v3_resource_relation('application_credentials'): {
'href-template': APPLICATION_CREDENTIALS,
'href-vars': {
'user_id': json_home.build_v3_parameter_relation('user_id')}},
json_home.build_v3_resource_relation('application_credential'): {
'href-template': APPLICATION_CREDENTIAL,
'href-vars': {