Convert S3 and EC2 auth to flask native dispatching

Convert S3 and EC2 auth to flask native dispatching.

Test changes required:

* Eliminate direct reference of the EC2 / S3 controllers, originally
  this direct reference was to verify signature checking. Since
  signature checking is an @staticmethod now, direct reference of
  the API resources covers everything.

* Direct import of keystone.common.controller - due to an oddity in
  how our WSGI code work(s) in test, if nothing imports the common
  controller module, the tests fail using the oslo import_class
  mechanism.

Change-Id: I06e95957b3ea3a55b0da28959548bd5eb628c70b
Partial-Bug: #1776504
This commit is contained in:
Morgan Fainberg 2018-10-09 17:15:50 -07:00
parent 0ab08e75b2
commit 35c9bb7eff
18 changed files with 377 additions and 480 deletions

View File

@ -14,6 +14,7 @@ from keystone.api import auth
from keystone.api import credentials
from keystone.api import discovery
from keystone.api import domains
from keystone.api import ec2tokens
from keystone.api import endpoints
from keystone.api import groups
from keystone.api import limits
@ -30,6 +31,7 @@ from keystone.api import registered_limits
from keystone.api import role_assignments
from keystone.api import role_inferences
from keystone.api import roles
from keystone.api import s3tokens
from keystone.api import services
from keystone.api import system
from keystone.api import trusts
@ -40,6 +42,7 @@ __all__ = (
'discovery',
'credentials',
'domains',
'ec2tokens',
'endpoints',
'groups',
'limits',
@ -56,6 +59,7 @@ __all__ = (
'role_assignments',
'role_inferences',
'roles',
's3tokens',
'services',
'system',
'trusts',
@ -67,6 +71,7 @@ __apis__ = (
auth,
credentials,
domains,
ec2tokens,
endpoints,
groups,
limits,
@ -83,6 +88,7 @@ __apis__ = (
role_assignments,
role_inferences,
roles,
s3tokens,
services,
system,
trusts,

View File

@ -0,0 +1,130 @@
# 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.
# Common base resource for EC2 and S3 Authentication
import sys
from oslo_serialization import jsonutils
import six
from werkzeug import exceptions
from keystone.common import provider_api
from keystone.common import utils
from keystone import exception as ks_exceptions
from keystone.i18n import _
from keystone.server import flask as ks_flask
PROVIDERS = provider_api.ProviderAPIs
CRED_TYPE_EC2 = 'ec2'
class ResourceBase(ks_flask.ResourceBase):
collection_key = '__UNUSED__'
member_key = '__UNUSED__'
def get(self):
# SPECIAL CASE: GET is not allowed, raise METHOD_NOT_ALLOWED
raise exceptions.MethodNotAllowed(valid_methods=['POST'])
@staticmethod
def _check_signature(cred_ref, credentials):
# NOTE(morgan): @staticmethod doesn't always play nice with
# the ABC module.
raise NotImplementedError()
def handle_authenticate(self):
# TODO(morgan): convert this dirty check to JSON Schema validation
# this mirrors the previous behavior of the webob system where an
# empty request body for s3 and ec2 tokens would result in a BAD
# REQUEST. Almost all other APIs use JSON Schema and therefore would
# catch this early on. S3 and EC2 did not ever get json schema
# implemented for them.
if not self.request_body_json:
msg = _('request must include a request body')
raise ks_exceptions.ValidationError(msg)
# NOTE(morgan): THIS IS SLOPPY! Apparently... keystone passed values
# as "credential" and "credentials" in into the s3/ec2 authenticate
# methods. There is no reason the multiple names should have worked
# except that we totally did something wonky in the past... so now
# there are 2 dirty "acceptable" body hacks for compatibility....
# Try "credentials" then "credential" and THEN ec2Credentials. Final
# default is {}
credentials = (
self.request_body_json.get('credentials') or
self.request_body_json.get('credential') or
self.request_body_json.get('ec2Credentials')
)
if not credentials:
credentials = {}
if 'access' not in credentials:
raise ks_exceptions.Unauthorized(_('EC2 Signature not supplied'))
# Load the credential from the backend
credential_id = utils.hash_access_key(credentials['access'])
cred = PROVIDERS.credential_api.get_credential(credential_id)
if not cred or cred['type'] != CRED_TYPE_EC2:
raise ks_exceptions.Unauthorized(_('EC2 access key not found.'))
# load from json if needed
try:
loaded = jsonutils.loads(cred['blob'])
except TypeError:
loaded = cred['blob']
# Convert to the legacy format
cred_data = dict(
user_id=cred.get('user_id'),
tenant_id=cred.get('project_id'),
access=loaded.get('access'),
secret=loaded.get('secret'),
trust_id=loaded.get('trust_id')
)
# validate the signature
self._check_signature(cred_data, credentials)
project_ref = PROVIDERS.resource_api.get_project(
cred_data['tenant_id'])
user_ref = PROVIDERS.identity_api.get_user(cred_data['user_id'])
# validate that the auth info is valid and nothing is disabled
try:
PROVIDERS.identity_api.assert_user_enabled(
user_id=user_ref['id'], user=user_ref)
PROVIDERS.resource_api.assert_project_enabled(
project_id=project_ref['id'], project=project_ref)
except AssertionError as e:
six.reraise(
ks_exceptions.Unauthorized,
ks_exceptions.Unauthorized(e),
sys.exc_info()[2])
roles = PROVIDERS.assignment_api.get_roles_for_user_and_project(
user_ref['id'], project_ref['id'])
if not roles:
raise ks_exceptions.Unauthorized(_('User not valid for project.'))
for r_id in roles:
# Assert all roles exist.
PROVIDERS.role_api.get_role(r_id)
method_names = ['ec2credential']
token = PROVIDERS.token_provider_api.issue_token(
user_id=user_ref['id'], method_names=method_names,
project_id=project_ref['id'])
return token

View File

@ -24,6 +24,11 @@ os_ec2_resource_rel_func = functools.partial(
json_home.build_v3_extension_resource_relation,
extension_name='OS-EC2', extension_version='1.0')
# s3token "extension"
s3_token_resource_rel_func = functools.partial(
json_home.build_v3_extension_resource_relation,
extension_name='s3tokens', extension_version='1.0')
# OS-EP-FILTER "extension"
os_ep_filter_resource_rel_func = functools.partial(
json_home.build_v3_extension_resource_relation,

94
keystone/api/ec2tokens.py Normal file
View File

@ -0,0 +1,94 @@
# 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/ec2tokens
import flask
from keystoneclient.contrib.ec2 import utils as ec2_utils
from oslo_serialization import jsonutils
from six.moves import http_client
from keystone.api._shared import EC2_S3_Resource
from keystone.api._shared import json_home_relations
from keystone.common import render_token
from keystone.common import utils
from keystone import exception
from keystone.i18n import _
from keystone.server import flask as ks_flask
CRED_TYPE_EC2 = 'ec2'
class EC2TokensResource(EC2_S3_Resource.ResourceBase):
collection_key = '__UNUSED__'
member_key = '__UNUSED__'
@staticmethod
def _check_signature(creds_ref, credentials):
signer = ec2_utils.Ec2Signer(creds_ref['secret'])
signature = signer.generate(credentials)
# NOTE(davechecn): credentials.get('signature') is not guaranteed to
# exist, we need to check it explicitly.
if credentials.get('signature'):
if utils.auth_str_equal(credentials['signature'], signature):
return True
# NOTE(vish): Some client libraries don't use the port when
# signing requests, so try again without the port.
elif ':' in credentials['host']:
hostname, _port = credentials.split(':')
credentials['host'] = hostname
# NOTE(davechen): we need to reinitialize 'signer' to avoid
# contaminated status of signature, this is similar with
# other programming language libraries, JAVA for example.
signer = ec2_utils.Ec2Signer(creds_ref['secret'])
signature = signer.generate(credentials)
if utils.auth_str_equal(
credentials['signature'], signature):
return True
raise exception.Unauthorized(_('Invalid EC2 signature.'))
# Raise the exception when credentials.get('signature') is None
else:
raise exception.Unauthorized(
_('EC2 signature not supplied.'))
@ks_flask.unenforced_api
def post(self):
"""Authenticate ec2 token.
POST /v3/ec2tokens
"""
token = self.handle_authenticate()
token_reference = render_token.render_token_response_from_model(token)
resp_body = jsonutils.dumps(token_reference)
response = flask.make_response(resp_body, http_client.OK)
response.headers['X-Subject-Token'] = token.id
response.headers['Content-Type'] = 'application/json'
return response
class EC2TokensAPI(ks_flask.APIBase):
_name = 'ec2tokens'
_import_name = __name__
resources = []
resource_mapping = [
ks_flask.construct_resource_map(
resource=EC2TokensResource,
url='/ec2tokens',
resource_kwargs={},
rel='ec2tokens',
resource_relation_func=(
json_home_relations.os_ec2_resource_rel_func))
]
APIs = (EC2TokensAPI,)

123
keystone/api/s3tokens.py Normal file
View File

@ -0,0 +1,123 @@
# 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/s3tokens
import base64
import hashlib
import hmac
import flask
from oslo_serialization import jsonutils
import six
from six.moves import http_client
from keystone.api._shared import EC2_S3_Resource
from keystone.api._shared import json_home_relations
from keystone.common import render_token
from keystone.common import utils
from keystone import exception
from keystone.i18n import _
from keystone.server import flask as ks_flask
def _calculate_signature_v1(string_to_sign, secret_key):
"""Calculate a v1 signature.
:param bytes string_to_sign: String that contains request params and
is used for calculate signature of request
:param text secret_key: Second auth key of EC2 account that is used to
sign requests
"""
key = str(secret_key).encode('utf-8')
if six.PY2:
b64_encode = base64.encodestring
else:
b64_encode = base64.encodebytes
signed = b64_encode(hmac.new(key, string_to_sign, hashlib.sha1)
.digest()).decode('utf-8').strip()
return signed
def _calculate_signature_v4(string_to_sign, secret_key):
"""Calculate a v4 signature.
:param bytes string_to_sign: String that contains request params and
is used for calculate signature of request
:param text secret_key: Second auth key of EC2 account that is used to
sign requests
"""
parts = string_to_sign.split(b'\n')
if len(parts) != 4 or parts[0] != b'AWS4-HMAC-SHA256':
raise exception.Unauthorized(message=_('Invalid EC2 signature.'))
scope = parts[2].split(b'/')
if len(scope) != 4 or scope[2] != b's3' or scope[3] != b'aws4_request':
raise exception.Unauthorized(message=_('Invalid EC2 signature.'))
def _sign(key, msg):
return hmac.new(key, msg, hashlib.sha256).digest()
signed = _sign(('AWS4' + secret_key).encode('utf-8'), scope[0])
signed = _sign(signed, scope[1])
signed = _sign(signed, scope[2])
signed = _sign(signed, b'aws4_request')
signature = hmac.new(signed, string_to_sign, hashlib.sha256)
return signature.hexdigest()
class S3Resource(EC2_S3_Resource.ResourceBase):
@staticmethod
def _check_signature(creds_ref, credentials):
string_to_sign = base64.urlsafe_b64decode(str(credentials['token']))
if string_to_sign[0:4] != b'AWS4':
signature = _calculate_signature_v1(string_to_sign,
creds_ref['secret'])
else:
signature = _calculate_signature_v4(string_to_sign,
creds_ref['secret'])
if not utils.auth_str_equal(credentials['signature'], signature):
raise exception.Unauthorized(
message=_('Credential signature mismatch'))
@ks_flask.unenforced_api
def post(self):
"""Authenticate s3token.
POST /v3/s3tokens
"""
token = self.handle_authenticate()
token_reference = render_token.render_token_response_from_model(token)
resp_body = jsonutils.dumps(token_reference)
response = flask.make_response(resp_body, http_client.OK)
response.headers['Content-Type'] = 'application/json'
return response
class S3Api(ks_flask.APIBase):
_name = 's3tokens'
_import_name = __name__
resources = []
resource_mapping = [
ks_flask.construct_resource_map(
resource=S3Resource,
url='/s3tokens',
resource_kwargs={},
rel='s3tokens',
resource_relation_func=(
json_home_relations.s3_token_resource_rel_func))
]
APIs = (S3Api,)

View File

@ -1,18 +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.
from keystone.contrib.ec2 import controllers # noqa
from keystone.contrib.ec2.core import * # noqa
from keystone.contrib.ec2 import routers # noqa
from keystone.contrib.ec2.routers import Routers as Ec2ExtensionV3 # noqa

View File

@ -1,220 +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.
"""Main entry point into the EC2 Credentials service.
This service allows the creation of access/secret credentials used for
the ec2 interop layer of OpenStack.
A user can create as many access/secret pairs, each of which is mapped to a
specific project. This is required because OpenStack supports a user
belonging to multiple projects, whereas the signatures created on ec2-style
requests don't allow specification of which project the user wishes to act
upon.
To complete the cycle, we provide a method that OpenStack services can
use to validate a signature and get a corresponding OpenStack token. This
token allows method calls to other services within the context the
access/secret was created. As an example, Nova requests Keystone to validate
the signature of a request, receives a token, and then makes a request to
Glance to list images needed to perform the requested task.
"""
import abc
import sys
from keystoneclient.contrib.ec2 import utils as ec2_utils
from oslo_serialization import jsonutils
import six
from six.moves import http_client
from keystone.common import controller
from keystone.common import provider_api
from keystone.common import render_token
from keystone.common import utils
from keystone.common import wsgi
import keystone.conf
from keystone import exception
from keystone.i18n import _
CRED_TYPE_EC2 = 'ec2'
CONF = keystone.conf.CONF
PROVIDERS = provider_api.ProviderAPIs
@six.add_metaclass(abc.ABCMeta)
class Ec2ControllerCommon(provider_api.ProviderAPIMixin, object):
def check_signature(self, creds_ref, credentials):
signer = ec2_utils.Ec2Signer(creds_ref['secret'])
signature = signer.generate(credentials)
# NOTE(davechen): credentials.get('signature') is not guaranteed to
# exist, we need check it explicitly.
if credentials.get('signature'):
if utils.auth_str_equal(credentials['signature'], signature):
return True
# NOTE(vish): Some client libraries don't use the port when signing
# requests, so try again without port.
elif ':' in credentials['host']:
hostname, _port = credentials['host'].split(':')
credentials['host'] = hostname
# NOTE(davechen): we need reinitialize 'signer' to avoid
# contaminated status of signature, this is similar with
# other programming language libraries, JAVA for example.
signer = ec2_utils.Ec2Signer(creds_ref['secret'])
signature = signer.generate(credentials)
if utils.auth_str_equal(credentials['signature'],
signature):
return True
raise exception.Unauthorized(
message=_('Invalid EC2 signature.'))
else:
raise exception.Unauthorized(
message=_('EC2 signature not supplied.'))
# Raise the exception when credentials.get('signature') is None
else:
raise exception.Unauthorized(
message=_('EC2 signature not supplied.'))
@abc.abstractmethod
def authenticate(self, context, credentials=None, ec2Credentials=None):
"""Validate a signed EC2 request and provide a token.
Other services (such as Nova) use this **admin** call to determine
if a request they signed received is from a valid user.
If it is a valid signature, an OpenStack token that maps
to the user/tenant is returned to the caller, along with
all the other details returned from a normal token validation
call.
The returned token is useful for making calls to other
OpenStack services within the context of the request.
:param context: standard context
:param credentials: dict of ec2 signature
:param ec2Credentials: DEPRECATED dict of ec2 signature
:returns: token: OpenStack token equivalent to access key along
with the corresponding service catalog and roles
"""
raise exception.NotImplemented()
def _authenticate(self, credentials=None, ec2credentials=None):
"""Common code shared between the V2 and V3 authenticate methods.
:returns: user_ref, tenant_ref, roles_ref
"""
# FIXME(ja): validate that a service token was used!
# NOTE(termie): backwards compat hack
if not credentials and ec2credentials:
credentials = ec2credentials
if 'access' not in credentials:
raise exception.Unauthorized(
message=_('EC2 signature not supplied.'))
creds_ref = self._get_credentials(credentials['access'])
self.check_signature(creds_ref, credentials)
# TODO(termie): don't create new tokens every time
# TODO(termie): this is copied from TokenController.authenticate
tenant_ref = self.resource_api.get_project(creds_ref['tenant_id'])
user_ref = self.identity_api.get_user(creds_ref['user_id'])
# Validate that the auth info is valid and nothing is disabled
try:
self.identity_api.assert_user_enabled(
user_id=user_ref['id'], user=user_ref)
self.resource_api.assert_domain_enabled(
domain_id=user_ref['domain_id'])
self.resource_api.assert_project_enabled(
project_id=tenant_ref['id'], project=tenant_ref)
except AssertionError as e:
six.reraise(exception.Unauthorized, exception.Unauthorized(e),
sys.exc_info()[2])
roles = self.assignment_api.get_roles_for_user_and_project(
user_ref['id'], tenant_ref['id']
)
if not roles:
raise exception.Unauthorized(
message=_('User not valid for tenant.'))
roles_ref = [self.role_api.get_role(role_id) for role_id in roles]
return user_ref, tenant_ref, roles_ref
@staticmethod
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 _get_credentials(self, credential_id):
"""Return credentials from an ID.
:param credential_id: id of credential
:raises keystone.exception.Unauthorized: when credential id is invalid
or when the credential type is not ec2
:returns: credential: dict of ec2 credential.
"""
ec2_credential_id = utils.hash_access_key(credential_id)
cred = self.credential_api.get_credential(ec2_credential_id)
if not cred or cred['type'] != CRED_TYPE_EC2:
raise exception.Unauthorized(
message=_('EC2 access key not found.'))
return self._convert_v3_to_ec2_credential(cred)
def render_token_data_response(self, token_id, token_data):
"""Render token data HTTP response.
Stash token ID into the X-Subject-Token header.
"""
status = (http_client.OK,
http_client.responses[http_client.OK])
headers = [('X-Subject-Token', token_id)]
return wsgi.render_response(body=token_data,
status=status,
headers=headers)
class Ec2ControllerV3(Ec2ControllerCommon, controller.V3Controller):
collection_name = 'credentials'
member_name = 'credential'
def authenticate(self, context, credentials=None, ec2Credentials=None):
(user_ref, project_ref, roles_ref) = self._authenticate(
credentials=credentials, ec2credentials=ec2Credentials
)
method_names = ['ec2credential']
token = self.token_provider_api.issue_token(
user_ref['id'], method_names, project_id=project_ref['id']
)
token_reference = render_token.render_token_response_from_model(token)
return self.render_token_data_response(token.id, token_reference)

View File

@ -1,34 +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.
from keystone.common import extension
EXTENSION_DATA = {
'name': 'OpenStack EC2 API',
'namespace': 'https://docs.openstack.org/identity/api/ext/'
'OS-EC2/v1.0',
'alias': 'OS-EC2',
'updated': '2013-07-07T12:00:0-00:00',
'description': 'OpenStack EC2 Credentials backend.',
'links': [
{
'rel': 'describedby',
'type': 'text/html',
'href': 'https://developer.openstack.org/'
'api-ref-identity-v2-ext.html',
}
]}
extension.register_admin_extension(EXTENSION_DATA['alias'], EXTENSION_DATA)
extension.register_public_extension(EXTENSION_DATA['alias'], EXTENSION_DATA)

View File

@ -1,38 +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.contrib.ec2 import controllers
build_resource_relation = functools.partial(
json_home.build_v3_extension_resource_relation, extension_name='OS-EC2',
extension_version='1.0')
class Routers(wsgi.RoutersBase):
_path_prefixes = ('ec2tokens',)
def append_v3_routers(self, mapper, routers):
ec2_controller = controllers.Ec2ControllerV3()
# validation
self._add_resource(
mapper, ec2_controller,
path='/ec2tokens',
post_action='authenticate',
rel=build_resource_relation(resource_name='ec2tokens'))

View File

@ -1,15 +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.
from keystone.contrib.s3.core import * # noqa

View File

@ -1,119 +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.
"""Main entry point into the S3 Credentials service.
This service provides S3 token validation for services configured with the
s3_token middleware to authorize S3 requests.
This service uses the same credentials used by EC2. Refer to the documentation
for the EC2 module for how to generate the required credentials.
"""
import base64
import hashlib
import hmac
import six
from six.moves import http_client
from keystone.common import json_home
from keystone.common import utils
from keystone.common import wsgi
from keystone.contrib.ec2 import controllers
from keystone import exception
from keystone.i18n import _
class S3Extension(wsgi.RoutersBase):
_path_prefixes = ('s3tokens',)
def append_v3_routers(self, mapper, routers):
controller = S3Controller()
# validation
self._add_resource(
mapper, controller,
path='/s3tokens',
post_action='authenticate',
rel=json_home.build_v3_extension_resource_relation(
's3tokens', '1.0', 's3tokens'))
class S3Controller(controllers.Ec2ControllerV3):
def check_signature(self, creds_ref, credentials):
string_to_sign = base64.urlsafe_b64decode(str(credentials['token']))
if string_to_sign[0:4] != b'AWS4':
signature = self._calculate_signature_v1(string_to_sign,
creds_ref['secret'])
else:
signature = self._calculate_signature_v4(string_to_sign,
creds_ref['secret'])
if not utils.auth_str_equal(credentials['signature'], signature):
raise exception.Unauthorized(
message=_('Credential signature mismatch'))
def _calculate_signature_v1(self, string_to_sign, secret_key):
"""Calculate a v1 signature.
:param bytes string_to_sign: String that contains request params and
is used for calculate signature of request
:param text secret_key: Second auth key of EC2 account that is used to
sign requests
"""
key = str(secret_key).encode('utf-8')
if six.PY2:
b64_encode = base64.encodestring
else:
b64_encode = base64.encodebytes
signed = b64_encode(hmac.new(key, string_to_sign, hashlib.sha1)
.digest()).decode('utf-8').strip()
return signed
def _calculate_signature_v4(self, string_to_sign, secret_key):
"""Calculate a v4 signature.
:param bytes string_to_sign: String that contains request params and
is used for calculate signature of request
:param text secret_key: Second auth key of EC2 account that is used to
sign requests
"""
parts = string_to_sign.split(b'\n')
if len(parts) != 4 or parts[0] != b'AWS4-HMAC-SHA256':
raise exception.Unauthorized(message=_('Invalid EC2 signature.'))
scope = parts[2].split(b'/')
if len(scope) != 4 or scope[2] != b's3' or scope[3] != b'aws4_request':
raise exception.Unauthorized(message=_('Invalid EC2 signature.'))
def _sign(key, msg):
return hmac.new(key, msg, hashlib.sha256).digest()
signed = _sign(('AWS4' + secret_key).encode('utf-8'), scope[0])
signed = _sign(signed, scope[1])
signed = _sign(signed, scope[2])
signed = _sign(signed, b'aws4_request')
signature = hmac.new(signed, string_to_sign, hashlib.sha256)
return signature.hexdigest()
def render_token_data_response(self, token_id, token_data):
"""Render token data HTTP response.
Note: We neither want nor need to send back the token id.
"""
status = (http_client.OK,
http_client.responses[http_client.OK])
return wsgi.render_response(body=token_data, status=status)

View File

@ -1,16 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from keystone.contrib import s3
Routers = s3.S3Extension

View File

@ -25,8 +25,6 @@ import werkzeug.wsgi
import keystone.api
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
# TODO(morgan): _MOVED_API_PREFIXES to be removed when the legacy dispatch
# support is removed.
@ -34,6 +32,7 @@ _MOVED_API_PREFIXES = frozenset(
['auth',
'credentials',
'domains',
'ec2tokens',
'endpoints',
'groups',
'OS-EP-FILTER',
@ -51,6 +50,7 @@ _MOVED_API_PREFIXES = frozenset(
'role_assignments',
'role_inferences',
'roles',
's3tokens',
'services',
'system',
'users',
@ -60,8 +60,7 @@ _MOVED_API_PREFIXES = frozenset(
LOG = log.getLogger(__name__)
ALL_API_ROUTERS = [ec2_routers,
s3_routers]
ALL_API_ROUTERS = []
def fail_gracefully(f):

View File

@ -16,7 +16,6 @@ from keystoneclient.contrib.ec2 import utils as ec2_utils
from six.moves import http_client
from keystone.common import provider_api
from keystone.contrib.ec2 import controllers
from keystone.tests import unit
from keystone.tests.unit import test_v3
@ -33,8 +32,6 @@ class EC2ContribCoreV3(test_v3.RestfulTestCase):
PROVIDERS.credential_api.create_credential(
self.credential['id'], self.credential)
self.controller = controllers.Ec2ControllerV3
def test_valid_authentication_response_with_proper_secret(self):
signer = ec2_utils.Ec2Signer(self.cred_blob['secret'])
credentials = {

View File

@ -19,8 +19,8 @@ import uuid
from six.moves import http_client
from keystone.api import s3tokens
from keystone.common import provider_api
from keystone.contrib import s3
from keystone import exception
from keystone.tests import unit
from keystone.tests.unit import test_v3
@ -39,8 +39,6 @@ class S3ContribCore(test_v3.RestfulTestCase):
PROVIDERS.credential_api.create_credential(
self.credential['id'], self.credential)
self.controller = s3.S3Controller()
def test_good_response(self):
sts = 'string to sign' # opaque string from swift3
sig = hmac.new(self.cred_blob['secret'].encode('ascii'),
@ -91,8 +89,8 @@ class S3ContribCore(test_v3.RestfulTestCase):
'vbV9zMy50eHQ=',
'signature': 'IL4QLcLVaYgylF9iHj6Wb8BGZsw='}
self.assertIsNone(self.controller.check_signature(creds_ref,
credentials))
self.assertIsNone(s3tokens.S3Resource._check_signature(
creds_ref, credentials))
def test_bad_signature_v1(self):
creds_ref = {'secret':
@ -105,7 +103,7 @@ class S3ContribCore(test_v3.RestfulTestCase):
'signature': uuid.uuid4().hex}
self.assertRaises(exception.Unauthorized,
self.controller.check_signature,
s3tokens.S3Resource._check_signature,
creds_ref, credentials)
def test_good_signature_v4(self):
@ -120,8 +118,8 @@ class S3ContribCore(test_v3.RestfulTestCase):
'730ba8f58df6ffeadd78f402e990b2910d60'
'bc5c2aec63619734f096a4dd77be'}
self.assertIsNone(self.controller.check_signature(creds_ref,
credentials))
self.assertIsNone(s3tokens.S3Resource._check_signature(
creds_ref, credentials))
def test_bad_signature_v4(self):
creds_ref = {'secret':
@ -134,7 +132,7 @@ class S3ContribCore(test_v3.RestfulTestCase):
'signature': uuid.uuid4().hex}
self.assertRaises(exception.Unauthorized,
self.controller.check_signature,
s3tokens.S3Resource._check_signature,
creds_ref, credentials)
def test_bad_token_v4(self):
@ -145,7 +143,7 @@ class S3ContribCore(test_v3.RestfulTestCase):
'QVdTNC1BQUEKWApYClg=',
'signature': ''}
self.assertRaises(exception.Unauthorized,
self.controller.check_signature,
s3tokens.S3Resource._check_signature,
creds_ref, credentials)
# token has invalid format of scope
@ -153,5 +151,5 @@ class S3ContribCore(test_v3.RestfulTestCase):
'QVdTNC1ITUFDLVNIQTI1NgpYCi8vczMvYXdzTl9yZXF1ZXN0Clg=',
'signature': ''}
self.assertRaises(exception.Unauthorized,
self.controller.check_signature,
s3tokens.S3Resource._check_signature,
creds_ref, credentials)

View File

@ -20,9 +20,9 @@ from keystoneclient.contrib.ec2 import utils as ec2_utils
from six.moves import http_client
from testtools import matchers
from keystone.api import ec2tokens
from keystone.common import provider_api
from keystone.common import utils
from keystone.contrib.ec2 import controllers
from keystone.credential.providers import fernet as credential_fernet
from keystone import exception
from keystone.tests import unit
@ -31,7 +31,7 @@ from keystone.tests.unit import test_v3
PROVIDERS = provider_api.ProviderAPIs
CRED_TYPE_EC2 = controllers.CRED_TYPE_EC2
CRED_TYPE_EC2 = ec2tokens.CRED_TYPE_EC2
class CredentialBaseTestCase(test_v3.RestfulTestCase):

View File

@ -26,6 +26,11 @@ from six.moves import http_client
from testtools import matchers
import webob
# NOTE(morgan): we need to explicitly import the controller
# or a number of tests get really cranky due to the way they
# hit oslo_utils.import_class for the controller. This can
# go away once the final bits of WSGI compat code is removed.
from keystone.common import controller # noqa
from keystone.common import wsgi
from keystone import exception
from keystone.server.flask import core as server_flask