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:
parent
0ab08e75b2
commit
35c9bb7eff
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -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,)
|
|
@ -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,)
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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'))
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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):
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue