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 services
from keystone.api import system from keystone.api import system
from keystone.api import trusts from keystone.api import trusts
from keystone.api import users
__all__ = ( __all__ = (
'auth', 'auth',
@ -56,6 +57,7 @@ __all__ = (
'services', 'services',
'system', 'system',
'trusts', 'trusts',
'users',
) )
__apis__ = ( __apis__ = (
@ -81,4 +83,5 @@ __apis__ = (
services, services,
system, system,
trusts, trusts,
users,
) )

View File

@ -19,6 +19,11 @@ import functools
from keystone.common import json_home 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 "extension"
os_ep_filter_resource_rel_func = functools.partial( os_ep_filter_resource_rel_func = functools.partial(
json_home.build_v3_extension_resource_relation, 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 # License for the specific language governing permissions and limitations
# under the License. # under the License.
from keystone.application_credential import controllers # noqa
from keystone.application_credential.core import * # 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 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): class GrantAssignmentV3(controller.V3Controller):
"""The V3 Grant Assignment APIs.""" """The V3 Grant Assignment APIs."""

View File

@ -20,31 +20,12 @@ from keystone.common import json_home
from keystone.common import wsgi 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): class Routers(wsgi.RoutersBase):
_path_prefixes = ('users', 'projects') _path_prefixes = ('projects',)
def append_v3_routers(self, mapper, routers): 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() grant_controller = controllers.GrantAssignmentV3()
self._add_resource( self._add_resource(
mapper, grant_controller, mapper, grant_controller,

View File

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

View File

@ -34,7 +34,6 @@ Glance to list images needed to perform the requested task.
import abc import abc
import sys import sys
import uuid
from keystoneclient.contrib.ec2 import utils as ec2_utils from keystoneclient.contrib.ec2 import utils as ec2_utils
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
@ -156,70 +155,6 @@ class Ec2ControllerCommon(provider_api.ProviderAPIMixin, object):
return user_ref, tenant_ref, roles_ref 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 @staticmethod
def _convert_v3_to_ec2_credential(credential): def _convert_v3_to_ec2_credential(credential):
# Prior to bug #1259584 fix, blob was stored unserialized # Prior to bug #1259584 fix, blob was stored unserialized
@ -271,22 +206,6 @@ class Ec2ControllerV3(Ec2ControllerCommon, controller.V3Controller):
collection_name = 'credentials' collection_name = 'credentials'
member_name = 'credential' 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): def authenticate(self, context, credentials=None, ec2Credentials=None):
(user_ref, project_ref, roles_ref) = self._authenticate( (user_ref, project_ref, roles_ref) = self._authenticate(
credentials=credentials, ec2credentials=ec2Credentials credentials=credentials, ec2credentials=ec2Credentials
@ -299,37 +218,3 @@ class Ec2ControllerV3(Ec2ControllerCommon, controller.V3Controller):
) )
token_reference = render_token.render_token_response_from_model(token) token_reference = render_token.render_token_response_from_model(token)
return self.render_token_data_response(token.id, token_reference) 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): class Routers(wsgi.RoutersBase):
_path_prefixes = ('ec2tokens', 'users') _path_prefixes = ('ec2tokens',)
def append_v3_routers(self, mapper, routers): def append_v3_routers(self, mapper, routers):
ec2_controller = controllers.Ec2ControllerV3() ec2_controller = controllers.Ec2ControllerV3()
@ -36,25 +36,3 @@ class Routers(wsgi.RoutersBase):
path='/ec2tokens', path='/ec2tokens',
post_action='authenticate', post_action='authenticate',
rel=build_resource_relation(resource_name='ec2tokens')) 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 # License for the specific language governing permissions and limitations
# under the License. # under the License.
from keystone.identity import controllers # noqa
from keystone.identity.core import * # noqa from keystone.identity.core import * # noqa
from keystone.identity import generator # 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 # - select the right driver for this domain
# - clear/set domain_ids for drivers that do not support domains # - clear/set domain_ids for drivers that do not support domains
# - create any ID mapping that might be required # - 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') @notifications.emit_event('authenticate')
def authenticate(self, user_id, password):
return self._authenticate(user_id, password)
@domains_configured @domains_configured
@exception_translated('assertion') @exception_translated('assertion')
def _authenticate(self, user_id, password): def authenticate(self, user_id, password):
domain_id, driver, entity_id = ( domain_id, driver, entity_id = (
self._get_domain_driver_and_entity_id(user_id)) self._get_domain_driver_and_entity_id(user_id))
ref = driver.authenticate(entity_id, password) ref = driver.authenticate(entity_id, password)
@ -1390,9 +1380,7 @@ class Manager(manager.Manager):
# authenticate() will raise an AssertionError if authentication fails # authenticate() will raise an AssertionError if authentication fails
try: try:
# TODO(morgan): When users is ported to flask, ensure this is self.authenticate(user_id, original_password)
# mapped back to self.authenticate instead of self._authenticate.
self._authenticate(user_id, original_password)
except exception.PasswordExpired: except exception.PasswordExpired:
# If a password has expired, we want users to be able to change it # If a password has expired, we want users to be able to change it
pass 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 werkzeug.wsgi
import keystone.api import keystone.api
from keystone.application_credential import routers as app_cred_routers
from keystone.assignment import routers as assignment_routers from keystone.assignment import routers as assignment_routers
from keystone.common import wsgi as keystone_wsgi from keystone.common import wsgi as keystone_wsgi
from keystone.contrib.ec2 import routers as ec2_routers from keystone.contrib.ec2 import routers as ec2_routers
from keystone.contrib.s3 import routers as s3_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 from keystone.resource import routers as resource_routers
# TODO(morgan): _MOVED_API_PREFIXES to be removed when the legacy dispatch # TODO(morgan): _MOVED_API_PREFIXES to be removed when the legacy dispatch
@ -57,6 +54,7 @@ _MOVED_API_PREFIXES = frozenset(
'roles', 'roles',
'services', 'services',
'system', 'system',
'users',
] ]
) )
@ -64,10 +62,7 @@ LOG = log.getLogger(__name__)
ALL_API_ROUTERS = [assignment_routers, ALL_API_ROUTERS = [assignment_routers,
identity_routers,
app_cred_routers,
resource_routers, resource_routers,
oauth1_routers,
ec2_routers, ec2_routers,
s3_routers] s3_routers]

View File

@ -921,7 +921,7 @@ class ResourceBase(flask_restful.Resource):
if token_ref.domain_scoped: if token_ref.domain_scoped:
return token_ref.domain_id return token_ref.domain_id
elif token_ref.project_scoped: elif token_ref.project_scoped:
return token_ref.project_domain_id return token_ref.project_domain['id']
else: else:
msg = 'No domain information specified as part of list request' msg = 'No domain information specified as part of list request'
tr_msg = _('No domain information specified as part of list ' 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 # Retrieve the auth context that was prepared by
# AuthContextMiddleware. # AuthContextMiddleware.
auth_context = cls.auth_context auth_context = flask.request.environ.get(
authorization.AUTH_CONTEXT_ENV, {})
return auth_context['token'] return auth_context['token']
except KeyError: except KeyError:
LOG.warning("Couldn't find the auth context.") 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) expected_status=http_client.NO_CONTENT)
def test_update_application_credential(self): def test_update_application_credential(self):
roles = [{'id': self.role_id}] with self.test_client() as c:
app_cred_body = self._app_cred_body(roles=roles) roles = [{'id': self.role_id}]
resp = self.post('/users/%s/application_credentials' % self.user_id, app_cred_body = self._app_cred_body(roles=roles)
body=app_cred_body, token = self.get_scoped_token()
expected_status=http_client.CREATED) resp = c.post(
# Application credentials are immutable '/v3/users/%s/application_credentials' % self.user_id,
app_cred_body['application_credential']['description'] = "New Things" json=app_cred_body,
app_cred_id = resp.json['application_credential']['id'] expected_status_code=http_client.CREATED,
self.patch(MEMBER_PATH_FMT % {'user_id': self.user_id, headers={'X-Auth-Token': token})
'app_cred_id': app_cred_id}, # Application credentials are immutable
body=app_cred_body, app_cred_body['application_credential'][
expected_status=http_client.NOT_FOUND) '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 = ('/users/{user_id}/application_credentials/'
'{application_credential_id}') '{application_credential_id}')
APPLICATION_CREDENTIALS = '/users/{user_id}/application_credentials'
APPLICATION_CREDENTIAL_RELATION = ( APPLICATION_CREDENTIAL_RELATION = (
json_home.build_v3_parameter_relation('application_credential_id')) json_home.build_v3_parameter_relation('application_credential_id'))
@ -633,6 +634,10 @@ V3_JSON_HOME_RESOURCES = {
'href': '/limits/model', 'href': '/limits/model',
'hints': {'status': 'experimental'} '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'): { json_home.build_v3_resource_relation('application_credential'): {
'href-template': APPLICATION_CREDENTIAL, 'href-template': APPLICATION_CREDENTIAL,
'href-vars': { 'href-vars': {