Add delegated_auth support for keystone
Implements an OAuth 1.0a service provider. blueprint: delegated-auth-via-oauth DocImpact SecurityImpact Change-Id: Ib5561593ab608f3b22fbcd7196e2171f95b735e8
This commit is contained in:
parent
81534a182a
commit
bcaa3072f3
@ -72,6 +72,7 @@ following sections:
|
||||
* ``[sql]`` - optional storage backend configuration
|
||||
* ``[ec2]`` - Amazon EC2 authentication driver configuration
|
||||
* ``[s3]`` - Amazon S3 authentication driver configuration.
|
||||
* ``[oauth1]`` - Oauth 1.0a system driver configuration
|
||||
* ``[identity]`` - identity system driver configuration
|
||||
* ``[catalog]`` - service catalog driver configuration
|
||||
* ``[token]`` - token driver & token provider configuration
|
||||
|
@ -24,6 +24,9 @@ paste.filter_factory = keystone.contrib.admin_crud:CrudExtension.factory
|
||||
[filter:ec2_extension]
|
||||
paste.filter_factory = keystone.contrib.ec2:Ec2Extension.factory
|
||||
|
||||
[filter:oauth_extension]
|
||||
paste.filter_factory = keystone.contrib.oauth1.routers:OAuth1Extension.factory
|
||||
|
||||
[filter:s3_extension]
|
||||
paste.filter_factory = keystone.contrib.s3:S3Extension.factory
|
||||
|
||||
|
@ -163,6 +163,16 @@
|
||||
[assignment]
|
||||
# driver =
|
||||
|
||||
[oauth1]
|
||||
# driver = keystone.contrib.oauth1.backends.sql.OAuth1
|
||||
|
||||
# The Identity service may include expire attributes.
|
||||
# If no such attribute is included, then the token lasts indefinitely.
|
||||
# Specify how quickly the request token will expire (in seconds)
|
||||
# request_token_duration = 28800
|
||||
# Specify how quickly the access token will expire (in seconds)
|
||||
# access_token_duration = 86400
|
||||
|
||||
[ssl]
|
||||
#enable = True
|
||||
#certfile = /etc/keystone/pki/certs/ssl_cert.pem
|
||||
@ -289,10 +299,11 @@
|
||||
# user_additional_attribute_mapping =
|
||||
|
||||
[auth]
|
||||
methods = external,password,token
|
||||
methods = external,password,token,oauth1
|
||||
#external = keystone.auth.plugins.external.ExternalDefault
|
||||
password = keystone.auth.plugins.password.Password
|
||||
token = keystone.auth.plugins.token.Token
|
||||
oauth1 = keystone.auth.plugins.oauth1.OAuth
|
||||
|
||||
[paste_deploy]
|
||||
# Name of the paste configuration file that defines the available pipelines
|
||||
|
@ -285,6 +285,8 @@ class Auth(controller.V3Controller):
|
||||
auth_info = AuthInfo(context, auth=auth)
|
||||
auth_context = {'extras': {}, 'method_names': [], 'bind': {}}
|
||||
self.authenticate(context, auth_info, auth_context)
|
||||
if auth_context.get('access_token_id'):
|
||||
auth_info.set_scope(None, auth_context['project_id'], None)
|
||||
self._check_and_set_default_scoping(auth_info, auth_context)
|
||||
(domain_id, project_id, trust) = auth_info.get_scope()
|
||||
method_names = auth_info.get_method_names()
|
||||
|
80
keystone/auth/plugins/oauth1.py
Normal file
80
keystone/auth/plugins/oauth1.py
Normal file
@ -0,0 +1,80 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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.
|
||||
|
||||
from keystone import auth
|
||||
from keystone.common import dependency
|
||||
from keystone.common import logging
|
||||
from keystone.contrib import oauth1
|
||||
from keystone.contrib.oauth1 import core as oauth
|
||||
from keystone import exception
|
||||
from keystone.openstack.common import timeutils
|
||||
|
||||
|
||||
METHOD_NAME = 'oauth1'
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dependency.requires('oauth_api')
|
||||
class OAuth(auth.AuthMethodHandler):
|
||||
def __init__(self):
|
||||
self.oauth_api = oauth1.Manager()
|
||||
|
||||
def authenticate(self, context, auth_info, auth_context):
|
||||
"""Turn a signed request with an access key into a keystone token."""
|
||||
headers = context['headers']
|
||||
oauth_headers = oauth.get_oauth_headers(headers)
|
||||
consumer_id = oauth_headers.get('oauth_consumer_key')
|
||||
access_token_id = oauth_headers.get('oauth_token')
|
||||
|
||||
if not access_token_id:
|
||||
raise exception.ValidationError(
|
||||
attribute='oauth_token', target='request')
|
||||
|
||||
acc_token = self.oauth_api.get_access_token(access_token_id)
|
||||
consumer = self.oauth_api._get_consumer(consumer_id)
|
||||
|
||||
expires_at = acc_token['expires_at']
|
||||
if expires_at:
|
||||
now = timeutils.utcnow()
|
||||
expires = timeutils.normalize_time(
|
||||
timeutils.parse_isotime(expires_at))
|
||||
if now > expires:
|
||||
raise exception.Unauthorized(_('Access token is expired'))
|
||||
|
||||
consumer_obj = oauth1.Consumer(key=consumer['id'],
|
||||
secret=consumer['secret'])
|
||||
acc_token_obj = oauth1.Token(key=acc_token['id'],
|
||||
secret=acc_token['access_secret'])
|
||||
|
||||
url = oauth.rebuild_url(context['path'])
|
||||
oauth_request = oauth1.Request.from_request(
|
||||
http_method='POST',
|
||||
http_url=url,
|
||||
headers=context['headers'],
|
||||
query_string=context['query_string'])
|
||||
oauth_server = oauth1.Server()
|
||||
oauth_server.add_signature_method(oauth1.SignatureMethod_HMAC_SHA1())
|
||||
params = oauth_server.verify_request(oauth_request,
|
||||
consumer_obj,
|
||||
token=acc_token_obj)
|
||||
|
||||
if len(params) != 0:
|
||||
msg = _('There should not be any non-oauth parameters')
|
||||
raise exception.Unauthorized(message=msg)
|
||||
|
||||
auth_context['user_id'] = acc_token['authorizing_user_id']
|
||||
auth_context['access_token_id'] = access_token_id
|
||||
auth_context['project_id'] = acc_token['project_id']
|
@ -37,6 +37,12 @@ class Token(auth.AuthMethodHandler):
|
||||
target=METHOD_NAME)
|
||||
token_id = auth_payload['id']
|
||||
token_ref = self.token_api.get_token(token_id)
|
||||
if ('OS-TRUST:trust' in token_ref['token_data']['token'] or
|
||||
'trust' in token_ref['token_data']['token']):
|
||||
raise exception.Forbidden()
|
||||
if 'OS-OAUTH1' in token_ref['token_data']['token']:
|
||||
raise exception.Forbidden()
|
||||
|
||||
wsgi.validate_token_bind(context, token_ref)
|
||||
user_context.setdefault(
|
||||
'user_id', token_ref['token_data']['token']['user']['id'])
|
||||
@ -48,9 +54,6 @@ class Token(auth.AuthMethodHandler):
|
||||
token_ref['token_data']['token']['extras'])
|
||||
user_context['method_names'].extend(
|
||||
token_ref['token_data']['token']['methods'])
|
||||
if ('OS-TRUST:trust' in token_ref['token_data']['token'] or
|
||||
'trust' in token_ref['token_data']['token']):
|
||||
raise exception.Forbidden()
|
||||
except AssertionError as e:
|
||||
LOG.error(e)
|
||||
raise exception.Unauthorized(e)
|
||||
|
@ -116,6 +116,11 @@ FILE_OPTIONS = {
|
||||
cfg.StrOpt('driver',
|
||||
default=('keystone.credential.backends'
|
||||
'.sql.Credential'))],
|
||||
'oauth1': [
|
||||
cfg.StrOpt('driver',
|
||||
default='keystone.contrib.oauth1.backends.sql.OAuth1'),
|
||||
cfg.IntOpt('request_token_duration', default=28800),
|
||||
cfg.IntOpt('access_token_duration', default=86400)],
|
||||
'policy': [
|
||||
cfg.StrOpt('driver',
|
||||
default='keystone.policy.backends.sql.Policy')],
|
||||
|
17
keystone/contrib/oauth1/__init__.py
Normal file
17
keystone/contrib/oauth1/__init__.py
Normal file
@ -0,0 +1,17 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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.
|
||||
|
||||
from keystone.contrib.oauth1.core import * # flake8: noqa
|
15
keystone/contrib/oauth1/backends/__init__.py
Normal file
15
keystone/contrib/oauth1/backends/__init__.py
Normal file
@ -0,0 +1,15 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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.
|
222
keystone/contrib/oauth1/backends/kvs.py
Normal file
222
keystone/contrib/oauth1/backends/kvs.py
Normal file
@ -0,0 +1,222 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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 datetime
|
||||
import random
|
||||
import uuid
|
||||
|
||||
from keystone.common import kvs
|
||||
from keystone.common import logging
|
||||
from keystone.contrib.oauth1 import core
|
||||
from keystone import exception
|
||||
from keystone.openstack.common import timeutils
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OAuth1(kvs.Base):
|
||||
"""kvs backend for oauth is deprecated.
|
||||
Deprecated in Havana and will be removed in Icehouse, as this backend
|
||||
is not production grade.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
super(OAuth1, self).__init__(*args, **kw)
|
||||
LOG.warn(_("kvs token backend is DEPRECATED. Use "
|
||||
"keystone.contrib.oauth1.sql instead."))
|
||||
|
||||
def _get_consumer(self, consumer_id):
|
||||
return self.db.get('consumer-%s' % consumer_id)
|
||||
|
||||
def get_consumer(self, consumer_id):
|
||||
consumer_ref = self.db.get('consumer-%s' % consumer_id)
|
||||
return core.filter_consumer(consumer_ref)
|
||||
|
||||
def create_consumer(self, consumer):
|
||||
consumer_id = consumer['id']
|
||||
consumer['secret'] = uuid.uuid4().hex
|
||||
if not consumer.get('description'):
|
||||
consumer['description'] = None
|
||||
self.db.set('consumer-%s' % consumer_id, consumer)
|
||||
consumer_list = set(self.db.get('consumer_list', []))
|
||||
consumer_list.add(consumer_id)
|
||||
self.db.set('consumer_list', list(consumer_list))
|
||||
return consumer
|
||||
|
||||
def _delete_consumer(self, consumer_id):
|
||||
# call get to make sure it exists
|
||||
self.db.get('consumer-%s' % consumer_id)
|
||||
self.db.delete('consumer-%s' % consumer_id)
|
||||
consumer_list = set(self.db.get('consumer_list', []))
|
||||
consumer_list.remove(consumer_id)
|
||||
self.db.set('consumer_list', list(consumer_list))
|
||||
|
||||
def _delete_request_tokens(self, consumer_id):
|
||||
consumer_requests = set(self.db.get('consumer-%s-requests' %
|
||||
consumer_id, []))
|
||||
for token in consumer_requests:
|
||||
self.db.get('request_token-%s' % token)
|
||||
self.db.delete('request_token-%s' % token)
|
||||
|
||||
if len(consumer_requests) > 0:
|
||||
self.db.delete('consumer-%s-requests' % consumer_id)
|
||||
|
||||
def _delete_access_tokens(self, consumer_id):
|
||||
consumer_accesses = set(self.db.get('consumer-%s-accesses' %
|
||||
consumer_id, []))
|
||||
for token in consumer_accesses:
|
||||
access_token = self.db.get('access_token-%s' % token)
|
||||
self.db.delete('access_token-%s' % token)
|
||||
|
||||
# kind of a hack, but I needed to update the auth_list
|
||||
user_id = access_token['authorizing_user_id']
|
||||
user_auth_list = set(self.db.get('auth_list-%s' % user_id, []))
|
||||
user_auth_list.remove(token)
|
||||
self.db.set('auth_list-%s' % user_id, list(user_auth_list))
|
||||
|
||||
if len(consumer_accesses) > 0:
|
||||
self.db.delete('consumer-%s-accesses' % consumer_id)
|
||||
|
||||
def delete_consumer(self, consumer_id):
|
||||
self._delete_consumer(consumer_id)
|
||||
self._delete_request_tokens(consumer_id)
|
||||
self._delete_access_tokens(consumer_id)
|
||||
|
||||
def list_consumers(self):
|
||||
consumer_ids = self.db.get('consumer_list', [])
|
||||
return [self.get_consumer(x) for x in consumer_ids]
|
||||
|
||||
def update_consumer(self, consumer_id, consumer):
|
||||
# call get to make sure it exists
|
||||
old_consumer_ref = self.db.get('consumer-%s' % consumer_id)
|
||||
new_consumer_ref = old_consumer_ref.copy()
|
||||
new_consumer_ref['description'] = consumer['description']
|
||||
new_consumer_ref['id'] = consumer_id
|
||||
self.db.set('consumer-%s' % consumer_id, new_consumer_ref)
|
||||
return new_consumer_ref
|
||||
|
||||
def create_request_token(self, consumer_id, roles,
|
||||
project_id, token_duration):
|
||||
expiry_date = None
|
||||
if token_duration:
|
||||
now = timeutils.utcnow()
|
||||
future = now + datetime.timedelta(seconds=token_duration)
|
||||
expiry_date = timeutils.isotime(future, subsecond=True)
|
||||
|
||||
ref = {}
|
||||
request_token_id = uuid.uuid4().hex
|
||||
ref['id'] = request_token_id
|
||||
ref['request_secret'] = uuid.uuid4().hex
|
||||
ref['verifier'] = None
|
||||
ref['authorizing_user_id'] = None
|
||||
ref['requested_project_id'] = project_id
|
||||
ref['requested_roles'] = roles
|
||||
ref['consumer_id'] = consumer_id
|
||||
ref['expires_at'] = expiry_date
|
||||
self.db.set('request_token-%s' % request_token_id, ref)
|
||||
|
||||
# add req token to the list that containers the consumers req tokens
|
||||
consumer_requests = set(self.db.get('consumer-%s-requests' %
|
||||
consumer_id, []))
|
||||
consumer_requests.add(request_token_id)
|
||||
self.db.set('consumer-%s-requests' %
|
||||
consumer_id, list(consumer_requests))
|
||||
return ref
|
||||
|
||||
def get_request_token(self, request_token_id):
|
||||
return self.db.get('request_token-%s' % request_token_id)
|
||||
|
||||
def authorize_request_token(self, request_token_id, user_id):
|
||||
request_token = self.db.get('request_token-%s' % request_token_id)
|
||||
request_token['authorizing_user_id'] = user_id
|
||||
request_token['verifier'] = str(random.randint(1000, 9999))
|
||||
self.db.set('request_token-%s' % request_token_id, request_token)
|
||||
return request_token
|
||||
|
||||
def create_access_token(self, request_id, token_duration):
|
||||
request_token = self.db.get('request_token-%s' % request_id)
|
||||
|
||||
expiry_date = None
|
||||
if token_duration:
|
||||
now = timeutils.utcnow()
|
||||
future = now + datetime.timedelta(seconds=token_duration)
|
||||
expiry_date = timeutils.isotime(future, subsecond=True)
|
||||
|
||||
ref = {}
|
||||
access_token_id = uuid.uuid4().hex
|
||||
ref['id'] = access_token_id
|
||||
ref['access_secret'] = uuid.uuid4().hex
|
||||
ref['authorizing_user_id'] = request_token['authorizing_user_id']
|
||||
ref['project_id'] = request_token['requested_project_id']
|
||||
ref['requested_roles'] = request_token['requested_roles']
|
||||
ref['consumer_id'] = request_token['consumer_id']
|
||||
ref['expires_at'] = expiry_date
|
||||
self.db.set('access_token-%s' % access_token_id, ref)
|
||||
|
||||
#add access token id to user authorizations list too
|
||||
user_id = request_token['authorizing_user_id']
|
||||
user_auth_list = set(self.db.get('auth_list-%s' % user_id, []))
|
||||
user_auth_list.add(access_token_id)
|
||||
self.db.set('auth_list-%s' % user_id, list(user_auth_list))
|
||||
|
||||
#delete request token from table, it has been exchanged
|
||||
self.db.get('request_token-%s' % request_id)
|
||||
self.db.delete('request_token-%s' % request_id)
|
||||
|
||||
#add access token to the list that containers the consumers acc tokens
|
||||
consumer_id = request_token['consumer_id']
|
||||
consumer_accesses = set(self.db.get('consumer-%s-accesses' %
|
||||
consumer_id, []))
|
||||
consumer_accesses.add(access_token_id)
|
||||
self.db.set('consumer-%s-accesses' %
|
||||
consumer_id, list(consumer_accesses))
|
||||
|
||||
# remove the used up request token id from consumer req list
|
||||
consumer_requests = set(self.db.get('consumer-%s-requests' %
|
||||
consumer_id, []))
|
||||
consumer_requests.remove(request_id)
|
||||
self.db.set('consumer-%s-requests' %
|
||||
consumer_id, list(consumer_requests))
|
||||
|
||||
return ref
|
||||
|
||||
def get_access_token(self, access_token_id):
|
||||
return self.db.get('access_token-%s' % access_token_id)
|
||||
|
||||
def list_access_tokens(self, user_id):
|
||||
user_auth_list = self.db.get('auth_list-%s' % user_id, [])
|
||||
return [self.get_access_token(x) for x in user_auth_list]
|
||||
|
||||
def delete_access_token(self, user_id, access_token_id):
|
||||
access_token = self.get_access_token(access_token_id)
|
||||
consumer_id = access_token['consumer_id']
|
||||
if access_token['authorizing_user_id'] != user_id:
|
||||
raise exception.Unauthorized(_('User IDs do not match'))
|
||||
self.db.get('access_token-%s' % access_token_id)
|
||||
self.db.delete('access_token-%s' % access_token_id)
|
||||
|
||||
# remove access token id from user authz list
|
||||
user_auth_list = set(self.db.get('auth_list-%s' % user_id, []))
|
||||
user_auth_list.remove(access_token_id)
|
||||
self.db.set('auth_list-%s' % user_id, list(user_auth_list))
|
||||
|
||||
# remove this token id from the consumer access list
|
||||
consumer_accesses = set(self.db.get('consumer-%s-accesses' %
|
||||
consumer_id, []))
|
||||
consumer_accesses.remove(access_token_id)
|
||||
self.db.set('consumer-%s-accesses' %
|
||||
consumer_id, list(consumer_accesses))
|
284
keystone/contrib/oauth1/backends/sql.py
Normal file
284
keystone/contrib/oauth1/backends/sql.py
Normal file
@ -0,0 +1,284 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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 datetime
|
||||
import random
|
||||
import uuid
|
||||
|
||||
from keystone.common import sql
|
||||
from keystone.common.sql import migration
|
||||
from keystone.contrib.oauth1 import core
|
||||
from keystone import exception
|
||||
from keystone.openstack.common import timeutils
|
||||
|
||||
|
||||
class Consumer(sql.ModelBase, sql.DictBase):
|
||||
__tablename__ = 'consumer'
|
||||
attributes = ['id', 'description', 'secret']
|
||||
id = sql.Column(sql.String(64), primary_key=True, nullable=False)
|
||||
description = sql.Column(sql.String(64), nullable=False)
|
||||
secret = sql.Column(sql.String(64), nullable=False)
|
||||
extra = sql.Column(sql.JsonBlob(), nullable=False)
|
||||
|
||||
|
||||
class RequestToken(sql.ModelBase, sql.DictBase):
|
||||
__tablename__ = 'request_token'
|
||||
attributes = ['id', 'request_secret',
|
||||
'verifier', 'authorizing_user_id', 'requested_project_id',
|
||||
'requested_roles', 'consumer_id', 'expires_at']
|
||||
id = sql.Column(sql.String(64), primary_key=True, nullable=False)
|
||||
request_secret = sql.Column(sql.String(64), nullable=False)
|
||||
verifier = sql.Column(sql.String(64), nullable=True)
|
||||
authorizing_user_id = sql.Column(sql.String(64), nullable=True)
|
||||
requested_project_id = sql.Column(sql.String(64), nullable=False)
|
||||
requested_roles = sql.Column(sql.Text(), nullable=False)
|
||||
consumer_id = sql.Column(sql.String(64), nullable=False, index=True)
|
||||
expires_at = sql.Column(sql.String(64), nullable=True)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, user_dict):
|
||||
return cls(**user_dict)
|
||||
|
||||
def to_dict(self):
|
||||
return dict(self.iteritems())
|
||||
|
||||
|
||||
class AccessToken(sql.ModelBase, sql.DictBase):
|
||||
__tablename__ = 'access_token'
|
||||
attributes = ['id', 'access_secret', 'authorizing_user_id',
|
||||
'project_id', 'requested_roles', 'consumer_id',
|
||||
'expires_at']
|
||||
id = sql.Column(sql.String(64), primary_key=True, nullable=False)
|
||||
access_secret = sql.Column(sql.String(64), nullable=False)
|
||||
authorizing_user_id = sql.Column(sql.String(64), nullable=False,
|
||||
index=True)
|
||||
project_id = sql.Column(sql.String(64), nullable=False)
|
||||
requested_roles = sql.Column(sql.Text(), nullable=False)
|
||||
consumer_id = sql.Column(sql.String(64), nullable=False)
|
||||
expires_at = sql.Column(sql.String(64), nullable=True)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, user_dict):
|
||||
return cls(**user_dict)
|
||||
|
||||
def to_dict(self):
|
||||
return dict(self.iteritems())
|
||||
|
||||
|
||||
class OAuth1(sql.Base):
|
||||
def db_sync(self):
|
||||
migration.db_sync()
|
||||
|
||||
def _get_consumer(self, consumer_id):
|
||||
session = self.get_session()
|
||||
consumer_ref = session.query(Consumer).get(consumer_id)
|
||||
if consumer_ref is None:
|
||||
raise exception.NotFound(_('Consumer not found'))
|
||||
return consumer_ref
|
||||
|
||||
def get_consumer(self, consumer_id):
|
||||
session = self.get_session()
|
||||
consumer_ref = session.query(Consumer).get(consumer_id)
|
||||
if consumer_ref is None:
|
||||
raise exception.NotFound(_('Consumer not found'))
|
||||
return core.filter_consumer(consumer_ref.to_dict())
|
||||
|
||||
def create_consumer(self, consumer):
|
||||
consumer['secret'] = uuid.uuid4().hex
|
||||
if not consumer.get('description'):
|
||||
consumer['description'] = None
|
||||
session = self.get_session()
|
||||
with session.begin():
|
||||
consumer_ref = Consumer.from_dict(consumer)
|
||||
session.add(consumer_ref)
|
||||
session.flush()
|
||||
return consumer_ref.to_dict()
|
||||
|
||||
def _delete_consumer(self, session, consumer_id):
|
||||
consumer_ref = self._get_consumer(session, consumer_id)
|
||||
q = session.query(Consumer)
|
||||
q = q.filter_by(id=consumer_id)
|
||||
q.delete(False)
|
||||
session.delete(consumer_ref)
|
||||
|
||||
def _delete_request_tokens(self, session, consumer_id):
|
||||
q = session.query(RequestToken)
|
||||
req_tokens = q.filter_by(consumer_id=consumer_id)
|
||||
req_tokens_list = set([x.id for x in req_tokens])
|
||||
for token_id in req_tokens_list:
|
||||
token_ref = self._get_request_token(session, token_id)
|
||||
q = session.query(RequestToken)
|
||||
q = q.filter_by(id=token_id)
|
||||
q.delete(False)
|
||||
session.delete(token_ref)
|
||||
|
||||
def _delete_access_tokens(self, session, consumer_id):
|
||||
q = session.query(AccessToken)
|
||||
acc_tokens = q.filter_by(consumer_id=consumer_id)
|
||||
acc_tokens_list = set([x.id for x in acc_tokens])
|
||||
for token_id in acc_tokens_list:
|
||||
token_ref = self._get_access_token(session, token_id)
|
||||
q = session.query(AccessToken)
|
||||
q = q.filter_by(id=token_id)
|
||||
q.delete(False)
|
||||
session.delete(token_ref)
|
||||
|
||||
def delete_consumer(self, consumer_id):
|
||||
session = self.get_session()
|
||||
with session.begin():
|
||||
self._delete_consumer(session, consumer_id)
|
||||
self._delete_request_tokens(session, consumer_id)
|
||||
self._delete_access_tokens(session, consumer_id)
|
||||
session.flush()
|
||||
|
||||
def list_consumers(self):
|
||||
session = self.get_session()
|
||||
cons = session.query(Consumer)
|
||||
return [core.filter_consumer(x.to_dict()) for x in cons]
|
||||
|
||||
def update_consumer(self, consumer_id, consumer):
|
||||
session = self.get_session()
|
||||
with session.begin():
|
||||
consumer_ref = self._get_consumer(consumer_id)
|
||||
old_consumer_dict = consumer_ref.to_dict()
|
||||
old_consumer_dict.update(consumer)
|
||||
new_consumer = Consumer.from_dict(old_consumer_dict)
|
||||
for attr in Consumer.attributes:
|
||||
if (attr != 'id' or attr != 'secret'):
|
||||
setattr(consumer_ref,
|
||||
attr,
|
||||
getattr(new_consumer, attr))
|
||||
consumer_ref.extra = new_consumer.extra
|
||||
session.flush()
|
||||
return core.filter_consumer(consumer_ref.to_dict())
|
||||
|
||||
def create_request_token(self, consumer_id, roles,
|
||||
project_id, token_duration):
|
||||
expiry_date = None
|
||||
if token_duration:
|
||||
now = timeutils.utcnow()
|
||||
future = now + datetime.timedelta(seconds=token_duration)
|
||||
expiry_date = timeutils.isotime(future, subsecond=True)
|
||||
|
||||
ref = {}
|
||||
request_token_id = uuid.uuid4().hex
|
||||
ref['id'] = request_token_id
|
||||
ref['request_secret'] = uuid.uuid4().hex
|
||||
ref['verifier'] = None
|
||||
ref['authorizing_user_id'] = None
|
||||
ref['requested_project_id'] = project_id
|
||||
ref['requested_roles'] = roles
|
||||
ref['consumer_id'] = consumer_id
|
||||
ref['expires_at'] = expiry_date
|
||||
session = self.get_session()
|
||||
with session.begin():
|
||||
token_ref = RequestToken.from_dict(ref)
|
||||
session.add(token_ref)
|
||||
session.flush()
|
||||
return token_ref.to_dict()
|
||||
|
||||
def _get_request_token(self, session, request_token_id):
|
||||
token_ref = session.query(RequestToken).get(request_token_id)
|
||||
if token_ref is None:
|
||||
raise exception.NotFound(_('Request token not found'))
|
||||
return token_ref
|
||||
|
||||
def get_request_token(self, request_token_id):
|
||||
session = self.get_session()
|
||||
token_ref = self._get_request_token(session, request_token_id)
|
||||
return token_ref.to_dict()
|
||||
|
||||
def authorize_request_token(self, request_token_id, user_id):
|
||||
session = self.get_session()
|
||||
with session.begin():
|
||||
token_ref = self._get_request_token(session, request_token_id)
|
||||
token_dict = token_ref.to_dict()
|
||||
token_dict['authorizing_user_id'] = user_id
|
||||
token_dict['verifier'] = str(random.randint(1000, 9999))
|
||||
|
||||
new_token = RequestToken.from_dict(token_dict)
|
||||
for attr in RequestToken.attributes:
|
||||
if (attr == 'authorizing_user_id' or attr == 'verifier'):
|
||||
setattr(token_ref, attr, getattr(new_token, attr))
|
||||
|
||||
session.flush()
|
||||
return token_ref.to_dict()
|
||||
|
||||
def create_access_token(self, request_token_id, token_duration):
|
||||
session = self.get_session()
|
||||
with session.begin():
|
||||
req_token_ref = self._get_request_token(session, request_token_id)
|
||||
token_dict = req_token_ref.to_dict()
|
||||
|
||||
expiry_date = None
|
||||
if token_duration:
|
||||
now = timeutils.utcnow()
|
||||
future = now + datetime.timedelta(seconds=token_duration)
|
||||
expiry_date = timeutils.isotime(future, subsecond=True)
|
||||
|
||||
# add Access Token
|
||||
ref = {}
|
||||
access_token_id = uuid.uuid4().hex
|
||||
ref['id'] = access_token_id
|
||||
ref['access_secret'] = uuid.uuid4().hex
|
||||
ref['authorizing_user_id'] = token_dict['authorizing_user_id']
|
||||
ref['project_id'] = token_dict['requested_project_id']
|
||||
ref['requested_roles'] = token_dict['requested_roles']
|
||||
ref['consumer_id'] = token_dict['consumer_id']
|
||||
ref['expires_at'] = expiry_date
|
||||
token_ref = AccessToken.from_dict(ref)
|
||||
session.add(token_ref)
|
||||
|
||||
# remove request token, it's been used
|
||||
q = session.query(RequestToken)
|
||||
q = q.filter_by(id=request_token_id)
|
||||
q.delete(False)
|
||||
session.delete(req_token_ref)
|
||||
|
||||
session.flush()
|
||||
return token_ref.to_dict()
|
||||
|
||||
def _get_access_token(self, session, access_token_id):
|
||||
token_ref = session.query(AccessToken).get(access_token_id)
|
||||
if token_ref is None:
|
||||
raise exception.NotFound(_('Access token not found'))
|
||||
return token_ref
|
||||
|
||||
def get_access_token(self, access_token_id):
|
||||
session = self.get_session()
|
||||
token_ref = self._get_access_token(session, access_token_id)
|
||||
return token_ref.to_dict()
|
||||
|
||||
def list_access_tokens(self, user_id):
|
||||
session = self.get_session()
|
||||
q = session.query(AccessToken)
|
||||
user_auths = q.filter_by(authorizing_user_id=user_id)
|
||||
return [core.filter_token(x.to_dict()) for x in user_auths]
|
||||
|
||||
def delete_access_token(self, user_id, access_token_id):
|
||||
session = self.get_session()
|
||||
with session.begin():
|
||||
token_ref = self._get_access_token(session, access_token_id)
|
||||
token_dict = token_ref.to_dict()
|
||||
if token_dict['authorizing_user_id'] != user_id:
|
||||
raise exception.Unauthorized(_('User IDs do not match'))
|
||||
|
||||
q = session.query(AccessToken)
|
||||
q = q.filter_by(id=access_token_id)
|
||||
q.delete(False)
|
||||
|
||||
session.delete(token_ref)
|
||||
session.flush()
|
377
keystone/contrib/oauth1/controllers.py
Normal file
377
keystone/contrib/oauth1/controllers.py
Normal file
@ -0,0 +1,377 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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 keystone.common import controller
|
||||
from keystone.common import dependency
|
||||
from keystone.common import wsgi
|
||||
from keystone import config
|
||||
from keystone.contrib.oauth1 import core as oauth1
|
||||
from keystone import exception
|
||||
from keystone.openstack.common import jsonutils
|
||||
from keystone.openstack.common import timeutils
|
||||
|
||||
|
||||
CONF = config.CONF
|
||||
|
||||
|
||||
@dependency.requires('oauth_api', 'token_api')
|
||||
class ConsumerCrudV3(controller.V3Controller):
|
||||
collection_name = 'consumers'
|
||||
member_name = 'consumer'
|
||||
|
||||
def create_consumer(self, context, consumer):
|
||||
ref = self._assign_unique_id(self._normalize_dict(consumer))
|
||||
consumer_ref = self.oauth_api.create_consumer(ref)
|
||||
return ConsumerCrudV3.wrap_member(context, consumer_ref)
|
||||
|
||||
def update_consumer(self, context, consumer_id, consumer):
|
||||
self._require_matching_id(consumer_id, consumer)
|
||||
ref = self._normalize_dict(consumer)
|
||||
self._validate_consumer_ref(consumer)
|
||||
ref = self.oauth_api.update_consumer(consumer_id, consumer)
|
||||
return ConsumerCrudV3.wrap_member(context, ref)
|
||||
|
||||
def list_consumers(self, context):
|
||||
ref = self.oauth_api.list_consumers()
|
||||
return ConsumerCrudV3.wrap_collection(context, ref)
|
||||
|
||||
def get_consumer(self, context, consumer_id):
|
||||
ref = self.oauth_api.get_consumer(consumer_id)
|
||||
return ConsumerCrudV3.wrap_member(context, ref)
|
||||
|
||||
def delete_consumer(self, context, consumer_id):
|
||||
user_token_ref = self.token_api.get_token(context['token_id'])
|
||||
user_id = user_token_ref['user'].get('id')
|
||||
self.token_api.delete_tokens(user_id, consumer_id=consumer_id)
|
||||
self.oauth_api.delete_consumer(consumer_id)
|
||||
|
||||
def _validate_consumer_ref(self, consumer):
|
||||
if 'secret' in consumer:
|
||||
msg = _('Cannot change consumer secret')
|
||||
raise exception.ValidationError(message=msg)
|
||||
|
||||
|
||||
@dependency.requires('oauth_api')
|
||||
class AccessTokenCrudV3(controller.V3Controller):
|
||||
collection_name = 'access_tokens'
|
||||
member_name = 'access_token'
|
||||
|
||||
def get_access_token(self, context, user_id, access_token_id):
|
||||
access_token = self.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(access_token)
|
||||
return AccessTokenCrudV3.wrap_member(context, access_token)
|
||||
|
||||
def list_access_tokens(self, context, user_id):
|
||||
refs = self.oauth_api.list_access_tokens(user_id)
|
||||
formatted_refs = ([self._format_token_entity(x) for x in refs])
|
||||
return AccessTokenCrudV3.wrap_collection(context, formatted_refs)
|
||||
|
||||
def delete_access_token(self, context, user_id, access_token_id):
|
||||
access_token = self.oauth_api.get_access_token(access_token_id)
|
||||
consumer_id = access_token['consumer_id']
|
||||
self.token_api.delete_tokens(user_id, consumer_id=consumer_id)
|
||||
return self.oauth_api.delete_access_token(
|
||||
user_id, access_token_id)
|
||||
|
||||
def _format_token_entity(self, entity):
|
||||
|
||||
formatted_entity = entity.copy()
|
||||
access_token_id = formatted_entity['id']
|
||||
user_id = ""
|
||||
if 'requested_roles' in entity:
|
||||
formatted_entity.pop('requested_roles')
|
||||
if 'access_secret' in entity:
|
||||
formatted_entity.pop('access_secret')
|
||||
if 'authorizing_user_id' in entity:
|
||||
user_id = formatted_entity['authorizing_user_id']
|
||||
|
||||
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(url))
|
||||
|
||||
return formatted_entity
|
||||
|
||||
|
||||
@dependency.requires('oauth_api')
|
||||
class AccessTokenRolesV3(controller.V3Controller):
|
||||
collection_name = 'roles'
|
||||
member_name = 'role'
|
||||
|
||||
def list_access_token_roles(self, context, user_id, access_token_id):
|
||||
access_token = self.oauth_api.get_access_token(access_token_id)
|
||||
if access_token['authorizing_user_id'] != user_id:
|
||||
raise exception.NotFound()
|
||||
roles = access_token['requested_roles']
|
||||
roles_refs = jsonutils.loads(roles)
|
||||
formatted_refs = ([self._format_role_entity(x) for x in roles_refs])
|
||||
return AccessTokenRolesV3.wrap_collection(context, formatted_refs)
|
||||
|
||||
def get_access_token_role(self, context, user_id,
|
||||
access_token_id, role_id):
|
||||
access_token = self.oauth_api.get_access_token(access_token_id)
|
||||
if access_token['authorizing_user_id'] != user_id:
|
||||
raise exception.Unauthorized(_('User IDs do not match'))
|
||||
roles = access_token['requested_roles']
|
||||
roles_dict = jsonutils.loads(roles)
|
||||
for role in roles_dict:
|
||||
if role['id'] == role_id:
|
||||
role = self._format_role_entity(role)
|
||||
return AccessTokenRolesV3.wrap_member(context, role)
|
||||
raise exception.RoleNotFound(_('Could not find role'))
|
||||
|
||||
def _format_role_entity(self, entity):
|
||||
|
||||
formatted_entity = entity.copy()
|
||||
if 'description' in entity:
|
||||
formatted_entity.pop('description')
|
||||
if 'enabled' in entity:
|
||||
formatted_entity.pop('enabled')
|
||||
return formatted_entity
|
||||
|
||||
|
||||
@dependency.requires('oauth_api', 'token_api', 'identity_api',
|
||||
'token_provider_api', 'assignment_api')
|
||||
class OAuthControllerV3(controller.V3Controller):
|
||||
collection_name = 'not_used'
|
||||
member_name = 'not_used'
|
||||
|
||||
def create_request_token(self, context):
|
||||
headers = context['headers']
|
||||
oauth_headers = oauth1.get_oauth_headers(headers)
|
||||
consumer_id = oauth_headers.get('oauth_consumer_key')
|
||||
requested_role_ids = headers.get('Requested-Role-Ids')
|
||||
requested_project_id = headers.get('Requested-Project-Id')
|
||||
if not consumer_id:
|
||||
raise exception.ValidationError(
|
||||
attribute='oauth_consumer_key', target='request')
|
||||
if not requested_role_ids:
|
||||
raise exception.ValidationError(
|
||||
attribute='requested_role_ids', target='request')
|
||||
if not requested_project_id:
|
||||
raise exception.ValidationError(
|
||||
attribute='requested_project_id', target='request')
|
||||
|
||||
req_role_ids = requested_role_ids.split(',')
|
||||
consumer_ref = self.oauth_api._get_consumer(consumer_id)
|
||||
consumer = oauth1.Consumer(key=consumer_ref['id'],
|
||||
secret=consumer_ref['secret'])
|
||||
|
||||
url = oauth1.rebuild_url(context['path'])
|
||||
oauth_request = oauth1.Request.from_request(
|
||||
http_method='POST',
|
||||
http_url=url,
|
||||
headers=context['headers'],
|
||||
query_string=context['query_string'],
|
||||
parameters={'requested_role_ids': requested_role_ids,
|
||||
'requested_project_id': requested_project_id})
|
||||
oauth_server = oauth1.Server()
|
||||
oauth_server.add_signature_method(oauth1.SignatureMethod_HMAC_SHA1())
|
||||
params = oauth_server.verify_request(oauth_request,
|
||||
consumer,
|
||||
token=None)
|
||||
|
||||
project_params = params['requested_project_id']
|
||||
if project_params != requested_project_id:
|
||||
msg = _('Non-oauth parameter - project, do not match')
|
||||
raise exception.Unauthorized(message=msg)
|
||||
|
||||
roles_params = params['requested_role_ids']
|
||||
roles_params_list = roles_params.split(',')
|
||||
if roles_params_list != req_role_ids:
|
||||
msg = _('Non-oauth parameter - roles, do not match')
|
||||
raise exception.Unauthorized(message=msg)
|
||||
|
||||
req_role_list = list()
|
||||
all_roles = self.identity_api.list_roles()
|
||||
for role in all_roles:
|
||||
for req_role in req_role_ids:
|
||||
if role['id'] == req_role:
|
||||
req_role_list.append(role)
|
||||
|
||||
if len(req_role_list) == 0:
|
||||
msg = _('could not find matching roles for provided role ids')
|
||||
raise exception.Unauthorized(message=msg)
|
||||
|
||||
json_roles = jsonutils.dumps(req_role_list)
|
||||
request_token_duration = CONF.oauth1.request_token_duration
|
||||
token_ref = self.oauth_api.create_request_token(consumer_id,
|
||||
json_roles,
|
||||
requested_project_id,
|
||||
request_token_duration)
|
||||
|
||||
result = ('oauth_token=%(key)s&oauth_token_secret=%(secret)s'
|
||||
% {'key': token_ref['id'],
|
||||
'secret': token_ref['request_secret']})
|
||||
|
||||
if CONF.oauth1.request_token_duration:
|
||||
expiry_bit = '&oauth_expires_at=%s' % token_ref['expires_at']
|
||||
result += expiry_bit
|
||||
|
||||
headers = [('Content-Type', 'application/x-www-urlformencoded')]
|
||||
response = wsgi.render_response(result,
|
||||
status=(201, 'Created'),
|
||||
headers=headers)
|
||||
|
||||
return response
|
||||
|
||||
def create_access_token(self, context):
|
||||
headers = context['headers']
|
||||
oauth_headers = oauth1.get_oauth_headers(headers)
|
||||
consumer_id = oauth_headers.get('oauth_consumer_key')
|
||||
request_token_id = oauth_headers.get('oauth_token')
|
||||
oauth_verifier = oauth_headers.get('oauth_verifier')
|
||||
|
||||
if not consumer_id:
|
||||
raise exception.ValidationError(
|
||||
attribute='oauth_consumer_key', target='request')
|
||||
if not request_token_id:
|
||||
raise exception.ValidationError(
|
||||
attribute='oauth_token', target='request')
|
||||
if not oauth_verifier:
|
||||
raise exception.ValidationError(
|
||||
attribute='oauth_verifier', target='request')
|
||||
|
||||
consumer = self.oauth_api._get_consumer(consumer_id)
|
||||
req_token = self.oauth_api.get_request_token(
|
||||
request_token_id)
|
||||
|
||||
expires_at = req_token['expires_at']
|
||||
if expires_at:
|
||||
now = timeutils.utcnow()
|
||||
expires = timeutils.normalize_time(
|
||||
timeutils.parse_isotime(expires_at))
|
||||
if now > expires:
|
||||
raise exception.Unauthorized(_('Request token is expired'))
|
||||
|
||||
consumer_obj = oauth1.Consumer(key=consumer['id'],
|
||||
secret=consumer['secret'])
|
||||
req_token_obj = oauth1.Token(key=req_token['id'],
|
||||
secret=req_token['request_secret'])
|
||||
req_token_obj.set_verifier(oauth_verifier)
|
||||
|
||||
url = oauth1.rebuild_url(context['path'])
|
||||
oauth_request = oauth1.Request.from_request(
|
||||
http_method='POST',
|
||||
http_url=url,
|
||||
headers=context['headers'],
|
||||
query_string=context['query_string'])
|
||||
oauth_server = oauth1.Server()
|
||||
oauth_server.add_signature_method(oauth1.SignatureMethod_HMAC_SHA1())
|
||||
params = oauth_server.verify_request(oauth_request,
|
||||
consumer_obj,
|
||||
token=req_token_obj)
|
||||
|
||||
if len(params) != 0:
|
||||
msg = _('There should not be any non-oauth parameters')
|
||||
raise exception.Unauthorized(message=msg)
|
||||
|
||||
if req_token['consumer_id'] != consumer_id:
|
||||
msg = _('provided consumer key does not match stored consumer key')
|
||||
raise exception.Unauthorized(message=msg)
|
||||
|
||||
if req_token['verifier'] != oauth_verifier:
|
||||
msg = _('provided verifier does not match stored verifier')
|
||||
raise exception.Unauthorized(message=msg)
|
||||
|
||||
if req_token['id'] != request_token_id:
|
||||
msg = _('provided request key does not match stored request key')
|
||||
raise exception.Unauthorized(message=msg)
|
||||
|
||||
if not req_token.get('authorizing_user_id'):
|
||||
msg = _('Request Token does not have an authorizing user id')
|
||||
raise exception.Unauthorized(message=msg)
|
||||
|
||||
access_token_duration = CONF.oauth1.access_token_duration
|
||||
token_ref = self.oauth_api.create_access_token(request_token_id,
|
||||
access_token_duration)
|
||||
|
||||
result = ('oauth_token=%(key)s&oauth_token_secret=%(secret)s'
|
||||
% {'key': token_ref['id'],
|
||||
'secret': token_ref['access_secret']})
|
||||
|
||||
if CONF.oauth1.access_token_duration:
|
||||
expiry_bit = '&oauth_expires_at=%s' % (token_ref['expires_at'])
|
||||
result += expiry_bit
|
||||
|
||||
headers = [('Content-Type', 'application/x-www-urlformencoded')]
|
||||
response = wsgi.render_response(result,
|
||||
status=(201, 'Created'),
|
||||
headers=headers)
|
||||
|
||||
return response
|
||||
|
||||
def authorize(self, context, request_token_id):
|
||||
"""An authenticated user is going to authorize a request token.
|
||||
|
||||
As a security precaution, the requested roles must match those in
|
||||
the request token. Because this is in a CLI-only world at the moment,
|
||||
there is not another easy way to make sure the user knows which roles
|
||||
are being requested before authorizing.
|
||||
"""
|
||||
|
||||
req_token = self.oauth_api.get_request_token(request_token_id)
|
||||
|
||||
expires_at = req_token['expires_at']
|
||||
if expires_at:
|
||||
now = timeutils.utcnow()
|
||||
expires = timeutils.normalize_time(
|
||||
timeutils.parse_isotime(expires_at))
|
||||
if now > expires:
|
||||
raise exception.Unauthorized(_('Request token is expired'))
|
||||
|
||||
req_roles = req_token['requested_roles']
|
||||
req_roles_list = jsonutils.loads(req_roles)
|
||||
|
||||
req_set = set()
|
||||
for x in req_roles_list:
|
||||
req_set.add(x['id'])
|
||||
|
||||
# verify the authorizing user has the roles
|
||||
user_token = self.token_api.get_token(token_id=context['token_id'])
|
||||
credentials = user_token['metadata'].copy()
|
||||
user_roles = credentials.get('roles')
|
||||
user_id = user_token['user'].get('id')
|
||||
cred_set = set(user_roles)
|
||||
|
||||
if not cred_set.issuperset(req_set):
|
||||
msg = _('authorizing user does not have role required')
|
||||
raise exception.Unauthorized(message=msg)
|
||||
|
||||
# verify the user has the project too
|
||||
req_project_id = req_token['requested_project_id']
|
||||
user_projects = self.assignment_api.list_user_projects(user_id)
|
||||
found = False
|
||||
for user_project in user_projects:
|
||||
if user_project['id'] == req_project_id:
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
msg = _("User is not a member of the requested project")
|
||||
raise exception.Unauthorized(message=msg)
|
||||
|
||||
# finally authorize the token
|
||||
authed_token = self.oauth_api.authorize_request_token(
|
||||
request_token_id, user_id)
|
||||
|
||||
to_return = {'token': {'oauth_verifier': authed_token['verifier']}}
|
||||
return to_return
|
272
keystone/contrib/oauth1/core.py
Normal file
272
keystone/contrib/oauth1/core.py
Normal file
@ -0,0 +1,272 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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 __future__ import absolute_import
|
||||
|
||||
import oauth2 as oauth
|
||||
|
||||
from keystone.common import dependency
|
||||
from keystone.common import extension
|
||||
from keystone.common import manager
|
||||
from keystone import config
|
||||
from keystone import exception
|
||||
|
||||
|
||||
Consumer = oauth.Consumer
|
||||
Request = oauth.Request
|
||||
Server = oauth.Server
|
||||
SignatureMethod = oauth.SignatureMethod
|
||||
SignatureMethod_HMAC_SHA1 = oauth.SignatureMethod_HMAC_SHA1
|
||||
SignatureMethod_PLAINTEXT = oauth.SignatureMethod_PLAINTEXT
|
||||
Token = oauth.Token
|
||||
Client = oauth.Client
|
||||
|
||||
|
||||
CONF = config.CONF
|
||||
|
||||
|
||||
EXTENSION_DATA = {
|
||||
'name': 'OpenStack OAUTH1 API',
|
||||
'namespace': 'http://docs.openstack.org/identity/api/ext/'
|
||||
'OS-OAUTH1/v1.0',
|
||||
'alias': 'OS-OAUTH1',
|
||||
'updated': '2013-07-07T12:00:0-00:00',
|
||||
'description': 'OpenStack OAuth 1.0a Delegated Auth Mechanism.',
|
||||
'links': [
|
||||
{
|
||||
'rel': 'describedby',
|
||||
# TODO(dolph): link needs to be revised after
|
||||
# bug 928059 merges
|
||||
'type': 'text/html',
|
||||
'href': 'https://github.com/openstack/identity-api',
|
||||
}
|
||||
]}
|
||||
extension.register_admin_extension(EXTENSION_DATA['alias'], EXTENSION_DATA)
|
||||
extension.register_public_extension(EXTENSION_DATA['alias'], EXTENSION_DATA)
|
||||
|
||||
|
||||
def filter_consumer(consumer_ref):
|
||||
"""Filter out private items in a consumer dict.
|
||||
|
||||
'secret' is never returned.
|
||||
|
||||
:returns: consumer_ref
|
||||
|
||||
"""
|
||||
if consumer_ref:
|
||||
consumer_ref = consumer_ref.copy()
|
||||
consumer_ref.pop('secret', None)
|
||||
return consumer_ref
|
||||
|
||||
|
||||
def filter_token(access_token_ref):
|
||||
"""Filter out private items in an access token dict.
|
||||
|
||||
'access_secret' is never returned.
|
||||
|
||||
:returns: access_token_ref
|
||||
|
||||
"""
|
||||
if access_token_ref:
|
||||
access_token_ref = access_token_ref.copy()
|
||||
access_token_ref.pop('access_secret', None)
|
||||
return access_token_ref
|
||||
|
||||
|
||||
def rebuild_url(path):
|
||||
endpoint = CONF.public_endpoint % CONF
|
||||
|
||||
# allow a missing trailing slash in the config
|
||||
if endpoint[-1] != '/':
|
||||
endpoint += '/'
|
||||
|
||||
url = endpoint + 'v3'
|
||||
return url + path
|
||||
|
||||
|
||||
def get_oauth_headers(headers):
|
||||
parameters = {}
|
||||
|
||||
# The incoming headers variable is your usual heading from context
|
||||
# In an OAuth signed req, where the oauth variables are in the header,
|
||||
# they with the key 'Authorization'.
|
||||
|
||||
if headers and 'Authorization' in headers:
|
||||
# A typical value for Authorization is seen below
|
||||
# 'OAuth realm="", oauth_body_hash="2jm%3D", oauth_nonce="14475435"
|
||||
# along with other oauth variables, the 'OAuth ' part is trimmed
|
||||
# to split the rest of the headers.
|
||||
|
||||
auth_header = headers['Authorization']
|
||||
# Check that the authorization header is OAuth.
|
||||
if auth_header[:6] == 'OAuth ':
|
||||
auth_header = auth_header[6:]
|
||||
# Get the parameters from the header.
|
||||
header_params = oauth.Request._split_header(auth_header)
|
||||
parameters.update(header_params)
|
||||
return parameters
|
||||
|
||||
|
||||
@dependency.provider('oauth_api')
|
||||
class Manager(manager.Manager):
|
||||
"""Default pivot point for the OAuth1 backend.
|
||||
|
||||
See :mod:`keystone.common.manager.Manager` for more details on how this
|
||||
dynamically calls the backend.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(Manager, self).__init__(CONF.oauth1.driver)
|
||||
|
||||
|
||||
class Driver(object):
|
||||
"""Interface description for an OAuth1 driver."""
|
||||
|
||||
def create_consumer(self, consumer_ref):
|
||||
"""Create consumer.
|
||||
|
||||
:param consumer_ref: consumer ref with consumer name
|
||||
:type consumer_ref: dict
|
||||
:returns: consumer_ref
|
||||
|
||||
"""
|
||||
raise exception.NotImplemented()
|
||||
|
||||
def update_consumer(self, consumer_id, consumer_ref):
|
||||
"""Update consumer.
|
||||
|
||||
:param consumer_id: id of consumer to update
|
||||
:type consumer_ref: string
|
||||
:param consumer_ref: new consumer ref with consumer name
|
||||
:type consumer_ref: dict
|
||||
:returns: consumer_ref
|
||||
|
||||
"""
|
||||
raise exception.NotImplemented()
|
||||
|
||||
def list_consumers(self):
|
||||
"""List consumers.
|
||||
|
||||
returns: list of consumers
|
||||
|
||||
"""
|
||||
raise exception.NotImplemented()
|
||||
|
||||
def get_consumer(self, consumer_id):
|
||||
"""Get consumer.
|
||||
|
||||
:param consumer_id: id of consumer to get
|
||||
:type consumer_ref: string
|
||||
:returns: consumer_ref
|
||||
|
||||
"""
|
||||
raise exception.NotImplemented()
|
||||
|
||||
def delete_consumer(self, consumer_id):
|
||||
"""Delete consumer.
|
||||
|
||||
:param consumer_id: id of consumer to get
|
||||
:type consumer_ref: string
|
||||
:returns: None.
|
||||
|
||||
"""
|
||||
raise exception.NotImplemented()
|
||||
|
||||
def list_access_tokens(self, user_id):
|
||||
"""List access tokens.
|
||||
|
||||
:param user_id: search for access tokens authorized by given user id
|
||||
:type user_id: string
|
||||
returns: list of access tokens the user has authorized
|
||||
|
||||
"""
|
||||
raise exception.NotImplemented()
|
||||
|
||||
def delete_access_token(self, user_id, access_token_id):
|
||||
"""Delete access token.
|
||||
|
||||
:param user_id: authorizing user id
|
||||
:type user_id: string
|
||||
:param access_token_id: access token to delete
|
||||
:type access_token_id: string
|
||||
returns: None
|
||||
|
||||
"""
|
||||
raise exception.NotImplemented()
|
||||
|
||||
def create_request_token(self, consumer_id, requested_roles,
|
||||
requested_project, request_token_duration):
|
||||
"""Create request token.
|
||||
|
||||
:param consumer_id: the id of the consumer
|
||||
:type consumer_id: string
|
||||
:param requested_roles: requested roles
|
||||
:type requested_roles: string
|
||||
:param requested_project_id: requested project id
|
||||
:type requested_project_id: string
|
||||
:param request_token_duration: duration of request token
|
||||
:type request_token_duration: string
|
||||
returns: request_token_ref
|
||||
|
||||
"""
|
||||
raise exception.NotImplemented()
|
||||
|
||||
def get_request_token(self, request_token_id):
|
||||
"""Get request token.
|
||||
|
||||
:param request_token_id: the id of the request token
|
||||
:type request_token_id: string
|
||||
returns: request_token_ref
|
||||
|
||||
"""
|
||||
raise exception.NotImplemented()
|
||||
|
||||
def get_access_token(self, access_token_id):
|
||||
"""Get access token.
|
||||
|
||||
:param access_token_id: the id of the access token
|
||||
:type access_token_id: string
|
||||
returns: access_token_ref
|
||||
|
||||
"""
|
||||
raise exception.NotImplemented()
|
||||
|
||||
def authorize_request_token(self, request_id, user_id):
|
||||
"""Authorize request token.
|
||||
|
||||
:param request_id: the id of the request token, to be authorized
|
||||
:type request_id: string
|
||||
:param user_id: the id of the authorizing user
|
||||
:type user_id: string
|
||||
returns: verifier
|
||||
|
||||
"""
|
||||
raise exception.NotImplemented()
|
||||
|
||||
def create_access_token(self, request_id, access_token_duration):
|
||||
"""Create access token.
|
||||
|
||||
:param request_id: the id of the request token, to be deleted
|
||||
:type request_id: string
|
||||
:param access_token_duration: duration of an access token
|
||||
:type access_token_duration: string
|
||||
returns: access_token_ref
|
||||
|
||||
"""
|
||||
raise exception.NotImplemented()
|
15
keystone/contrib/oauth1/migrate_repo/__init__.py
Normal file
15
keystone/contrib/oauth1/migrate_repo/__init__.py
Normal file
@ -0,0 +1,15 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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.
|
25
keystone/contrib/oauth1/migrate_repo/migrate.cfg
Normal file
25
keystone/contrib/oauth1/migrate_repo/migrate.cfg
Normal file
@ -0,0 +1,25 @@
|
||||
[db_settings]
|
||||
# Used to identify which repository this database is versioned under.
|
||||
# You can use the name of your project.
|
||||
repository_id=oauth1
|
||||
|
||||
# The name of the database table used to track the schema version.
|
||||
# This name shouldn't already be used by your project.
|
||||
# If this is changed once a database is under version control, you'll need to
|
||||
# change the table name in each database too.
|
||||
version_table=migrate_version
|
||||
|
||||
# When committing a change script, Migrate will attempt to generate the
|
||||
# sql for all supported databases; normally, if one of them fails - probably
|
||||
# because you don't have that database installed - it is ignored and the
|
||||
# commit continues, perhaps ending successfully.
|
||||
# Databases in this list MUST compile successfully during a commit, or the
|
||||
# entire commit will fail. List the databases your application will actually
|
||||
# be using to ensure your updates to that database work properly.
|
||||
# This must be a list; example: ['postgres','sqlite']
|
||||
required_dbs=[]
|
||||
|
||||
# When creating new change scripts, Migrate will stamp the new script with
|
||||
# a version number. By default this is latest_version + 1. You can set this
|
||||
# to 'true' to tell Migrate to use the UTC timestamp instead.
|
||||
use_timestamp_numbering=False
|
@ -0,0 +1,69 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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 sqlalchemy as sql
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
# Upgrade operations go here. Don't create your own engine; bind
|
||||
# migrate_engine to your metadata
|
||||
meta = sql.MetaData()
|
||||
meta.bind = migrate_engine
|
||||
|
||||
consumer_table = sql.Table(
|
||||
'consumer',
|
||||
meta,
|
||||
sql.Column('id', sql.String(64), primary_key=True, nullable=False),
|
||||
sql.Column('description', sql.String(64), nullable=False),
|
||||
sql.Column('secret', sql.String(64), nullable=False),
|
||||
sql.Column('extra', sql.Text(), nullable=False))
|
||||
consumer_table.create(migrate_engine, checkfirst=True)
|
||||
|
||||
request_token_table = sql.Table(
|
||||
'request_token',
|
||||
meta,
|
||||
sql.Column('id', sql.String(64), primary_key=True, nullable=False),
|
||||
sql.Column('request_secret', sql.String(64), nullable=False),
|
||||
sql.Column('verifier', sql.String(64), nullable=True),
|
||||
sql.Column('authorizing_user_id', sql.String(64), nullable=True),
|
||||
sql.Column('requested_project_id', sql.String(64), nullable=False),
|
||||
sql.Column('requested_roles', sql.Text(), nullable=False),
|
||||
sql.Column('consumer_id', sql.String(64), nullable=False, index=True),
|
||||
sql.Column('expires_at', sql.String(64), nullable=True))
|
||||
request_token_table.create(migrate_engine, checkfirst=True)
|
||||
|
||||
access_token_table = sql.Table(
|
||||
'access_token',
|
||||
meta,
|
||||
sql.Column('id', sql.String(64), primary_key=True, nullable=False),
|
||||
sql.Column('access_secret', sql.String(64), nullable=False),
|
||||
sql.Column('authorizing_user_id', sql.String(64),
|
||||
nullable=False, index=True),
|
||||
sql.Column('project_id', sql.String(64), nullable=False),
|
||||
sql.Column('requested_roles', sql.Text(), nullable=False),
|
||||
sql.Column('consumer_id', sql.String(64), nullable=False),
|
||||
sql.Column('expires_at', sql.String(64), nullable=True))
|
||||
access_token_table.create(migrate_engine, checkfirst=True)
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
meta = sql.MetaData()
|
||||
meta.bind = migrate_engine
|
||||
# Operations to reverse the above upgrade go here.
|
||||
tables = ['consumer', 'request_token', 'access_token']
|
||||
for table_name in tables:
|
||||
table = sql.Table(table_name, meta, autoload=True)
|
||||
table.drop()
|
15
keystone/contrib/oauth1/migrate_repo/versions/__init__.py
Normal file
15
keystone/contrib/oauth1/migrate_repo/versions/__init__.py
Normal file
@ -0,0 +1,15 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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.
|
129
keystone/contrib/oauth1/routers.py
Normal file
129
keystone/contrib/oauth1/routers.py
Normal file
@ -0,0 +1,129 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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.
|
||||
|
||||
from keystone.common import wsgi
|
||||
from keystone.contrib.oauth1 import controllers
|
||||
|
||||
|
||||
class OAuth1Extension(wsgi.ExtensionRouter):
|
||||
"""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:
|
||||
|
||||
# Basic admin-only consumer crud
|
||||
POST /OS-OAUTH1/consumers
|
||||
GET /OS-OAUTH1/consumers
|
||||
PATCH /OS-OAUTH1/consumers/$consumer_id
|
||||
GET /OS-OAUTH1/consumers/$consumer_id
|
||||
DELETE /OS-OAUTH1/consumers/$consumer_id
|
||||
|
||||
# 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
|
||||
|
||||
# OAuth interfaces
|
||||
POST /OS-OAUTH1/request_token # create a request token
|
||||
PUT /OS-OAUTH1/authorize # authorize a request token
|
||||
POST /OS-OAUTH1/access_token # create an access token
|
||||
|
||||
"""
|
||||
|
||||
def add_routes(self, mapper):
|
||||
consumer_controller = controllers.ConsumerCrudV3()
|
||||
access_token_controller = controllers.AccessTokenCrudV3()
|
||||
access_token_roles_controller = controllers.AccessTokenRolesV3()
|
||||
oauth_controller = controllers.OAuthControllerV3()
|
||||
|
||||
# basic admin-only consumer crud
|
||||
mapper.connect(
|
||||
'/OS-OAUTH1/consumers',
|
||||
controller=consumer_controller,
|
||||
action='create_consumer',
|
||||
conditions=dict(method=['POST']))
|
||||
mapper.connect(
|
||||
'/OS-OAUTH1/consumers/{consumer_id}',
|
||||
controller=consumer_controller,
|
||||
action='get_consumer',
|
||||
conditions=dict(method=['GET']))
|
||||
mapper.connect(
|
||||
'/OS-OAUTH1/consumers/{consumer_id}',
|
||||
controller=consumer_controller,
|
||||
action='update_consumer',
|
||||
conditions=dict(method=['PATCH']))
|
||||
mapper.connect(
|
||||
'/OS-OAUTH1/consumers/{consumer_id}',
|
||||
controller=consumer_controller,
|
||||
action='delete_consumer',
|
||||
conditions=dict(method=['DELETE']))
|
||||
mapper.connect(
|
||||
'/OS-OAUTH1/consumers',
|
||||
controller=consumer_controller,
|
||||
action='list_consumers',
|
||||
conditions=dict(method=['GET']))
|
||||
|
||||
# user accesss token crud
|
||||
mapper.connect(
|
||||
'/users/{user_id}/OS-OAUTH1/access_tokens',
|
||||
controller=access_token_controller,
|
||||
action='list_access_tokens',
|
||||
conditions=dict(method=['GET']))
|
||||
mapper.connect(
|
||||
'/users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id}',
|
||||
controller=access_token_controller,
|
||||
action='get_access_token',
|
||||
conditions=dict(method=['GET']))
|
||||
mapper.connect(
|
||||
'/users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id}',
|
||||
controller=access_token_controller,
|
||||
action='delete_access_token',
|
||||
conditions=dict(method=['DELETE']))
|
||||
mapper.connect(
|
||||
'/users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id}/roles',
|
||||
controller=access_token_roles_controller,
|
||||
action='list_access_token_roles',
|
||||
conditions=dict(method=['GET']))
|
||||
mapper.connect(
|
||||
'/users/{user_id}/OS-OAUTH1/access_tokens/'
|
||||
'{access_token_id}/roles/{role_id}',
|
||||
controller=access_token_roles_controller,
|
||||
action='get_access_token_role',
|
||||
conditions=dict(method=['GET']))
|
||||
|
||||
# oauth flow calls
|
||||
mapper.connect(
|
||||
'/OS-OAUTH1/request_token',
|
||||
controller=oauth_controller,
|
||||
action='create_request_token',
|
||||
conditions=dict(method=['POST']))
|
||||
mapper.connect(
|
||||
'/OS-OAUTH1/access_token',
|
||||
controller=oauth_controller,
|
||||
action='create_access_token',
|
||||
conditions=dict(method=['POST']))
|
||||
mapper.connect(
|
||||
'/OS-OAUTH1/authorize/{request_token_id}',
|
||||
controller=oauth_controller,
|
||||
action='authorize',
|
||||
conditions=dict(method=['PUT']))
|
@ -24,6 +24,7 @@ from keystone.common import dependency
|
||||
from keystone.common import wsgi
|
||||
from keystone import config
|
||||
from keystone.contrib import ec2
|
||||
from keystone.contrib import oauth1
|
||||
from keystone import controllers
|
||||
from keystone import credential
|
||||
from keystone import identity
|
||||
@ -49,6 +50,7 @@ DRIVERS = dict(
|
||||
credentials_api=credential.Manager(),
|
||||
ec2_api=ec2.Manager(),
|
||||
identity_api=_IDENTITY_API,
|
||||
oauth1_api=oauth1.Manager(),
|
||||
policy_api=policy.Manager(),
|
||||
token_api=token.Manager(),
|
||||
trust_api=trust.Manager(),
|
||||
|
@ -45,6 +45,7 @@ from keystone.common import utils
|
||||
from keystone.common import wsgi
|
||||
from keystone import config
|
||||
from keystone.contrib import ec2
|
||||
from keystone.contrib import oauth1
|
||||
from keystone import credential
|
||||
from keystone import exception
|
||||
from keystone import identity
|
||||
@ -268,7 +269,7 @@ class TestCase(NoModule, unittest.TestCase):
|
||||
# assignment manager gets the default assignment driver from the
|
||||
# identity driver.
|
||||
for manager in [identity, assignment, catalog, credential, ec2, policy,
|
||||
token, token_provider, trust]:
|
||||
token, token_provider, trust, oauth1]:
|
||||
# manager.__name__ is like keystone.xxx[.yyy],
|
||||
# converted to xxx[_yyy]
|
||||
manager_name = ('%s_api' %
|
||||
|
@ -3,6 +3,7 @@ import unittest2 as unittest
|
||||
|
||||
from keystone import assignment
|
||||
from keystone import catalog
|
||||
from keystone.contrib import oauth1
|
||||
from keystone import exception
|
||||
from keystone import identity
|
||||
from keystone import policy
|
||||
@ -55,3 +56,7 @@ class TestDrivers(unittest.TestCase):
|
||||
def test_token_driver_unimplemented(self):
|
||||
interface = token.Driver()
|
||||
self.assertInterfaceNotImplemented(interface)
|
||||
|
||||
def test_oauth1_driver_unimplemented(self):
|
||||
interface = oauth1.Driver()
|
||||
self.assertInterfaceNotImplemented(interface)
|
||||
|
@ -14,6 +14,9 @@ driver = keystone.trust.backends.kvs.Trust
|
||||
[token]
|
||||
driver = keystone.token.backends.kvs.Token
|
||||
|
||||
[oauth1]
|
||||
driver = keystone.contrib.oauth1.backends.kvs.OAuth1
|
||||
|
||||
[signing]
|
||||
certfile = ../../examples/pki/certs/signing_cert.pem
|
||||
keyfile = ../../examples/pki/private/signing_key.pem
|
||||
|
@ -27,6 +27,7 @@ To run these tests against a live database:
|
||||
"""
|
||||
|
||||
from keystone.contrib import example
|
||||
from keystone.contrib import oauth1
|
||||
|
||||
import test_sql_upgrade
|
||||
|
||||
@ -45,3 +46,65 @@ class SqlUpgradeExampleExtension(test_sql_upgrade.SqlMigrateBase):
|
||||
self.assertTableColumns('example', ['id', 'type', 'extra'])
|
||||
self.downgrade(0, repository=self.repo_path)
|
||||
self.assertTableDoesNotExist('example')
|
||||
|
||||
|
||||
class SqlUpgradeOAuth1Extension(test_sql_upgrade.SqlMigrateBase):
|
||||
def repo_package(self):
|
||||
return oauth1
|
||||
|
||||
def test_upgrade(self):
|
||||
self.assertTableDoesNotExist('consumer')
|
||||
self.assertTableDoesNotExist('request_token')
|
||||
self.assertTableDoesNotExist('access_token')
|
||||
self.upgrade(1, repository=self.repo_path)
|
||||
self.assertTableColumns('consumer',
|
||||
['id',
|
||||
'description',
|
||||
'secret',
|
||||
'extra'])
|
||||
self.assertTableColumns('request_token',
|
||||
['id',
|
||||
'request_secret',
|
||||
'verifier',
|
||||
'authorizing_user_id',
|
||||
'requested_project_id',
|
||||
'requested_roles',
|
||||
'consumer_id',
|
||||
'expires_at'])
|
||||
self.assertTableColumns('access_token',
|
||||
['id',
|
||||
'access_secret',
|
||||
'authorizing_user_id',
|
||||
'project_id',
|
||||
'requested_roles',
|
||||
'consumer_id',
|
||||
'expires_at'])
|
||||
|
||||
def test_downgrade(self):
|
||||
self.upgrade(1, repository=self.repo_path)
|
||||
self.assertTableColumns('consumer',
|
||||
['id',
|
||||
'description',
|
||||
'secret',
|
||||
'extra'])
|
||||
self.assertTableColumns('request_token',
|
||||
['id',
|
||||
'request_secret',
|
||||
'verifier',
|
||||
'authorizing_user_id',
|
||||
'requested_project_id',
|
||||
'requested_roles',
|
||||
'consumer_id',
|
||||
'expires_at'])
|
||||
self.assertTableColumns('access_token',
|
||||
['id',
|
||||
'access_secret',
|
||||
'authorizing_user_id',
|
||||
'project_id',
|
||||
'requested_roles',
|
||||
'consumer_id',
|
||||
'expires_at'])
|
||||
self.downgrade(0, repository=self.repo_path)
|
||||
self.assertTableDoesNotExist('consumer')
|
||||
self.assertTableDoesNotExist('request_token')
|
||||
self.assertTableDoesNotExist('access_token')
|
||||
|
574
keystone/tests/test_v3_oauth1.py
Normal file
574
keystone/tests/test_v3_oauth1.py
Normal file
@ -0,0 +1,574 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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 copy
|
||||
import os
|
||||
import urlparse
|
||||
import uuid
|
||||
|
||||
import webtest
|
||||
|
||||
from keystone.common import cms
|
||||
from keystone import config
|
||||
from keystone.contrib import oauth1
|
||||
from keystone.contrib.oauth1 import controllers
|
||||
from keystone.tests import core
|
||||
|
||||
import test_v3
|
||||
|
||||
|
||||
OAUTH_PASTE_FILE = 'v3_oauth1-paste.ini'
|
||||
CONF = config.CONF
|
||||
|
||||
|
||||
class OAuth1Tests(test_v3.RestfulTestCase):
|
||||
def setUp(self):
|
||||
super(OAuth1Tests, self).setUp()
|
||||
self.controller = controllers.OAuthControllerV3()
|
||||
self.base_url = CONF.public_endpoint % CONF + "v3"
|
||||
self._generate_paste_config()
|
||||
self.load_backends()
|
||||
self.admin_app = webtest.TestApp(
|
||||
self.loadapp('v3_oauth1', name='admin'))
|
||||
self.public_app = webtest.TestApp(
|
||||
self.loadapp('v3_oauth1', name='admin'))
|
||||
|
||||
def tearDown(self):
|
||||
os.remove(OAUTH_PASTE_FILE)
|
||||
|
||||
def _generate_paste_config(self):
|
||||
# Generate a file, based on keystone-paste.ini,
|
||||
# that includes oauth_extension in the pipeline
|
||||
old_pipeline = " ec2_extension "
|
||||
new_pipeline = " oauth_extension ec2_extension "
|
||||
|
||||
with open(core.etcdir('keystone-paste.ini'), 'r') as f:
|
||||
contents = f.read()
|
||||
new_contents = contents.replace(old_pipeline, new_pipeline)
|
||||
with open(OAUTH_PASTE_FILE, 'w') as f:
|
||||
f.write(new_contents)
|
||||
|
||||
def _create_single_consumer(self):
|
||||
ref = {'description': uuid.uuid4().hex}
|
||||
resp = self.post(
|
||||
'/OS-OAUTH1/consumers',
|
||||
body={'consumer': ref})
|
||||
return resp.result.get('consumer')
|
||||
|
||||
def _oauth_request(self, consumer, token=None, **kw):
|
||||
return oauth1.Request.from_consumer_and_token(consumer=consumer,
|
||||
token=token,
|
||||
**kw)
|
||||
|
||||
def _create_request_token(self, consumer, role, project_id):
|
||||
params = {'requested_role_ids': role,
|
||||
'requested_project_id': project_id}
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
url = '/OS-OAUTH1/request_token'
|
||||
oreq = self._oauth_request(
|
||||
consumer=consumer,
|
||||
http_url=self.base_url + url,
|
||||
http_method='POST',
|
||||
parameters=params)
|
||||
|
||||
hmac = oauth1.SignatureMethod_HMAC_SHA1()
|
||||
oreq.sign_request(hmac, consumer, None)
|
||||
headers.update(oreq.to_header())
|
||||
headers.update(params)
|
||||
return url, headers
|
||||
|
||||
def _create_access_token(self, consumer, token):
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
url = '/OS-OAUTH1/access_token'
|
||||
oreq = self._oauth_request(
|
||||
consumer=consumer, token=token,
|
||||
http_method='POST',
|
||||
http_url=self.base_url + url)
|
||||
hmac = oauth1.SignatureMethod_HMAC_SHA1()
|
||||
oreq.sign_request(hmac, consumer, token)
|
||||
headers.update(oreq.to_header())
|
||||
return url, headers
|
||||
|
||||
def _get_oauth_token(self, consumer, token):
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
body = {'auth': {'identity': {'methods': ['oauth1'], 'oauth1': {}}}}
|
||||
url = '/auth/tokens'
|
||||
oreq = self._oauth_request(
|
||||
consumer=consumer, token=token,
|
||||
http_method='POST',
|
||||
http_url=self.base_url + url)
|
||||
hmac = oauth1.SignatureMethod_HMAC_SHA1()
|
||||
oreq.sign_request(hmac, consumer, token)
|
||||
headers.update(oreq.to_header())
|
||||
return url, headers, body
|
||||
|
||||
def _authorize_request_token(self, request_id):
|
||||
return '/OS-OAUTH1/authorize/%s' % (request_id)
|
||||
|
||||
|
||||
class ConsumerCRUDTests(OAuth1Tests):
|
||||
|
||||
def test_consumer_create(self):
|
||||
description = uuid.uuid4().hex
|
||||
ref = {'description': description}
|
||||
resp = self.post(
|
||||
'/OS-OAUTH1/consumers',
|
||||
body={'consumer': ref})
|
||||
consumer = resp.result.get('consumer')
|
||||
consumer_id = consumer.get('id')
|
||||
self.assertEqual(consumer.get('description'), description)
|
||||
self.assertIsNotNone(consumer_id)
|
||||
self.assertIsNotNone(consumer.get('secret'))
|
||||
|
||||
def test_consumer_delete(self):
|
||||
consumer = self._create_single_consumer()
|
||||
consumer_id = consumer.get('id')
|
||||
resp = self.delete('/OS-OAUTH1/consumers/%(consumer_id)s'
|
||||
% {'consumer_id': consumer_id})
|
||||
self.assertResponseStatus(resp, 204)
|
||||
|
||||
def test_consumer_get(self):
|
||||
consumer = self._create_single_consumer()
|
||||
consumer_id = consumer.get('id')
|
||||
resp = self.get('/OS-OAUTH1/consumers/%(consumer_id)s'
|
||||
% {'consumer_id': consumer_id})
|
||||
self.assertTrue(resp.result.get('consumer').get('id'), consumer_id)
|
||||
|
||||
def test_consumer_list(self):
|
||||
resp = self.get('/OS-OAUTH1/consumers')
|
||||
entities = resp.result.get('consumers')
|
||||
self.assertIsNotNone(entities)
|
||||
self.assertValidListLinks(resp.result.get('links'))
|
||||
|
||||
def test_consumer_update(self):
|
||||
consumer = self._create_single_consumer()
|
||||
original_id = consumer.get('id')
|
||||
original_description = consumer.get('description')
|
||||
original_secret = consumer.get('secret')
|
||||
update_description = original_description + "_new"
|
||||
|
||||
update_ref = {'description': update_description}
|
||||
update_resp = self.patch('/OS-OAUTH1/consumers/%(consumer_id)s'
|
||||
% {'consumer_id': original_id},
|
||||
body={'consumer': update_ref})
|
||||
consumer = update_resp.result.get('consumer')
|
||||
self.assertEqual(consumer.get('description'), update_description)
|
||||
self.assertEqual(consumer.get('id'), original_id)
|
||||
self.assertEqual(consumer.get('secret'), original_secret)
|
||||
|
||||
def test_consumer_update_bad_secret(self):
|
||||
consumer = self._create_single_consumer()
|
||||
original_id = consumer.get('id')
|
||||
update_ref = copy.deepcopy(consumer)
|
||||
update_ref['description'] = uuid.uuid4().hex
|
||||
update_ref['secret'] = uuid.uuid4().hex
|
||||
self.patch('/OS-OAUTH1/consumers/%(consumer_id)s'
|
||||
% {'consumer_id': original_id},
|
||||
body={'consumer': update_ref},
|
||||
expected_status=400)
|
||||
|
||||
def test_consumer_update_bad_id(self):
|
||||
consumer = self._create_single_consumer()
|
||||
original_id = consumer.get('id')
|
||||
original_description = consumer.get('description')
|
||||
update_description = original_description + "_new"
|
||||
|
||||
update_ref = copy.deepcopy(consumer)
|
||||
update_ref['description'] = update_description
|
||||
update_ref['id'] = update_description
|
||||
self.patch('/OS-OAUTH1/consumers/%(consumer_id)s'
|
||||
% {'consumer_id': original_id},
|
||||
body={'consumer': update_ref},
|
||||
expected_status=400)
|
||||
|
||||
def test_consumer_create_no_description(self):
|
||||
resp = self.post('/OS-OAUTH1/consumers', body={'consumer': {}})
|
||||
consumer = resp.result.get('consumer')
|
||||
consumer_id = consumer.get('id')
|
||||
self.assertEqual(consumer.get('description'), None)
|
||||
self.assertIsNotNone(consumer_id)
|
||||
self.assertIsNotNone(consumer.get('secret'))
|
||||
|
||||
def test_consumer_get_bad_id(self):
|
||||
self.get('/OS-OAUTH1/consumers/%(consumer_id)s'
|
||||
% {'consumer_id': uuid.uuid4().hex},
|
||||
expected_status=404)
|
||||
|
||||
|
||||
class OAuthFlowTests(OAuth1Tests):
|
||||
|
||||
def test_oauth_flow(self):
|
||||
consumer = self._create_single_consumer()
|
||||
consumer_id = consumer.get('id')
|
||||
consumer_secret = consumer.get('secret')
|
||||
self.consumer = oauth1.Consumer(consumer_id, consumer_secret)
|
||||
self.assertIsNotNone(self.consumer.key)
|
||||
|
||||
url, headers = self._create_request_token(self.consumer,
|
||||
self.role_id,
|
||||
self.project_id)
|
||||
content = self.post(url, headers=headers)
|
||||
credentials = urlparse.parse_qs(content.result)
|
||||
request_key = credentials.get('oauth_token')[0]
|
||||
request_secret = credentials.get('oauth_token_secret')[0]
|
||||
self.request_token = oauth1.Token(request_key, request_secret)
|
||||
self.assertIsNotNone(self.request_token.key)
|
||||
|
||||
url = self._authorize_request_token(request_key)
|
||||
resp = self.put(url, expected_status=200)
|
||||
self.verifier = resp.result['token']['oauth_verifier']
|
||||
|
||||
self.request_token.set_verifier(self.verifier)
|
||||
url, headers = self._create_access_token(self.consumer,
|
||||
self.request_token)
|
||||
content = self.post(url, headers=headers)
|
||||
credentials = urlparse.parse_qs(content.result)
|
||||
access_key = credentials.get('oauth_token')[0]
|
||||
access_secret = credentials.get('oauth_token_secret')[0]
|
||||
self.access_token = oauth1.Token(access_key, access_secret)
|
||||
self.assertIsNotNone(self.access_token.key)
|
||||
|
||||
url, headers, body = self._get_oauth_token(self.consumer,
|
||||
self.access_token)
|
||||
content = self.post(url, headers=headers, body=body)
|
||||
self.keystone_token_id = content.headers.get('X-Subject-Token')
|
||||
self.keystone_token = content.result.get('token')
|
||||
self.assertIsNotNone(self.keystone_token_id)
|
||||
|
||||
|
||||
class AccessTokenCRUDTests(OAuthFlowTests):
|
||||
def test_delete_access_token_dne(self):
|
||||
self.delete('/users/%(user)s/OS-OAUTH1/access_tokens/%(auth)s'
|
||||
% {'user': self.user_id,
|
||||
'auth': uuid.uuid4().hex},
|
||||
expected_status=404)
|
||||
|
||||
def test_list_no_access_tokens(self):
|
||||
resp = self.get('/users/%(user_id)s/OS-OAUTH1/access_tokens'
|
||||
% {'user_id': self.user_id})
|
||||
entities = resp.result.get('access_tokens')
|
||||
self.assertTrue(len(entities) == 0)
|
||||
self.assertValidListLinks(resp.result.get('links'))
|
||||
|
||||
def test_get_single_access_token(self):
|
||||
self.test_oauth_flow()
|
||||
resp = self.get('/users/%(user_id)s/OS-OAUTH1/access_tokens/%(key)s'
|
||||
% {'user_id': self.user_id,
|
||||
'key': self.access_token.key})
|
||||
entity = resp.result.get('access_token')
|
||||
self.assertTrue(entity['id'], self.access_token.key)
|
||||
self.assertTrue(entity['consumer_id'], self.consumer.key)
|
||||
|
||||
def test_get_access_token_dne(self):
|
||||
self.get('/users/%(user_id)s/OS-OAUTH1/access_tokens/%(key)s'
|
||||
% {'user_id': self.user_id,
|
||||
'key': uuid.uuid4().hex},
|
||||
expected_status=404)
|
||||
|
||||
def test_list_all_roles_in_access_token(self):
|
||||
self.test_oauth_flow()
|
||||
resp = self.get('/users/%(id)s/OS-OAUTH1/access_tokens/%(key)s/roles'
|
||||
% {'id': self.user_id,
|
||||
'key': self.access_token.key})
|
||||
entities = resp.result.get('roles')
|
||||
self.assertTrue(len(entities) > 0)
|
||||
self.assertValidListLinks(resp.result.get('links'))
|
||||
|
||||
def test_get_role_in_access_token(self):
|
||||
self.test_oauth_flow()
|
||||
url = ('/users/%(id)s/OS-OAUTH1/access_tokens/%(key)s/roles/%(role)s'
|
||||
% {'id': self.user_id, 'key': self.access_token.key,
|
||||
'role': self.role_id})
|
||||
resp = self.get(url)
|
||||
entity = resp.result.get('role')
|
||||
self.assertTrue(entity['id'], self.role_id)
|
||||
|
||||
def test_get_role_in_access_token_dne(self):
|
||||
self.test_oauth_flow()
|
||||
url = ('/users/%(id)s/OS-OAUTH1/access_tokens/%(key)s/roles/%(role)s'
|
||||
% {'id': self.user_id, 'key': self.access_token.key,
|
||||
'role': uuid.uuid4().hex})
|
||||
self.get(url, expected_status=404)
|
||||
|
||||
def test_list_and_delete_access_tokens(self):
|
||||
self.test_oauth_flow()
|
||||
# List access_tokens should be > 0
|
||||
resp = self.get('/users/%(user_id)s/OS-OAUTH1/access_tokens'
|
||||
% {'user_id': self.user_id})
|
||||
entities = resp.result.get('access_tokens')
|
||||
self.assertTrue(len(entities) > 0)
|
||||
self.assertValidListLinks(resp.result.get('links'))
|
||||
|
||||
# Delete access_token
|
||||
resp = self.delete('/users/%(user)s/OS-OAUTH1/access_tokens/%(auth)s'
|
||||
% {'user': self.user_id,
|
||||
'auth': self.access_token.key})
|
||||
self.assertResponseStatus(resp, 204)
|
||||
|
||||
# List access_token should be 0
|
||||
resp = self.get('/users/%(user_id)s/OS-OAUTH1/access_tokens'
|
||||
% {'user_id': self.user_id})
|
||||
entities = resp.result.get('access_tokens')
|
||||
self.assertTrue(len(entities) == 0)
|
||||
self.assertValidListLinks(resp.result.get('links'))
|
||||
|
||||
|
||||
class AuthTokenTests(OAuthFlowTests):
|
||||
|
||||
def test_keystone_token_is_valid(self):
|
||||
self.test_oauth_flow()
|
||||
headers = {'X-Subject-Token': self.keystone_token_id,
|
||||
'X-Auth-Token': self.keystone_token_id}
|
||||
r = self.get('/auth/tokens', headers=headers)
|
||||
self.assertValidTokenResponse(r, self.user)
|
||||
|
||||
# now verify the oauth section
|
||||
oauth_section = r.result['token']['OS-OAUTH1']
|
||||
self.assertEquals(oauth_section['access_token_id'],
|
||||
self.access_token.key)
|
||||
self.assertEquals(oauth_section['consumer_id'], self.consumer.key)
|
||||
|
||||
def test_delete_access_token_also_revokes_token(self):
|
||||
self.test_oauth_flow()
|
||||
|
||||
# Delete access token
|
||||
resp = self.delete('/users/%(user)s/OS-OAUTH1/access_tokens/%(auth)s'
|
||||
% {'user': self.user_id,
|
||||
'auth': self.access_token.key})
|
||||
self.assertResponseStatus(resp, 204)
|
||||
|
||||
# Check Keystone Token no longer exists
|
||||
headers = {'X-Subject-Token': self.keystone_token_id,
|
||||
'X-Auth-Token': self.keystone_token_id}
|
||||
self.get('/auth/tokens', headers=headers,
|
||||
expected_status=401)
|
||||
|
||||
def test_deleting_consumer_also_deletes_tokens(self):
|
||||
self.test_oauth_flow()
|
||||
|
||||
# Delete consumer
|
||||
consumer_id = self.consumer.key
|
||||
resp = self.delete('/OS-OAUTH1/consumers/%(consumer_id)s'
|
||||
% {'consumer_id': consumer_id})
|
||||
self.assertResponseStatus(resp, 204)
|
||||
|
||||
# List access_token should be 0
|
||||
resp = self.get('/users/%(user_id)s/OS-OAUTH1/access_tokens'
|
||||
% {'user_id': self.user_id})
|
||||
entities = resp.result.get('access_tokens')
|
||||
self.assertEqual(len(entities), 0)
|
||||
|
||||
# Check Keystone Token no longer exists
|
||||
headers = {'X-Subject-Token': self.keystone_token_id,
|
||||
'X-Auth-Token': self.keystone_token_id}
|
||||
self.head('/auth/tokens', headers=headers,
|
||||
expected_status=401)
|
||||
|
||||
def test_change_user_password_also_deletes_tokens(self):
|
||||
self.test_oauth_flow()
|
||||
|
||||
# delegated keystone token exists
|
||||
headers = {'X-Subject-Token': self.keystone_token_id,
|
||||
'X-Auth-Token': self.keystone_token_id}
|
||||
r = self.get('/auth/tokens', headers=headers)
|
||||
self.assertValidTokenResponse(r, self.user)
|
||||
|
||||
user = {'password': uuid.uuid4().hex}
|
||||
r = self.patch('/users/%(user_id)s' % {
|
||||
'user_id': self.user['id']},
|
||||
body={'user': user})
|
||||
|
||||
headers = {'X-Subject-Token': self.keystone_token_id,
|
||||
'X-Auth-Token': self.keystone_token_id}
|
||||
self.admin_request(path='/auth/tokens', headers=headers,
|
||||
method='GET', expected_status=404)
|
||||
|
||||
def test_deleting_project_also_invalidates_tokens(self):
|
||||
self.test_oauth_flow()
|
||||
|
||||
# delegated keystone token exists
|
||||
headers = {'X-Subject-Token': self.keystone_token_id,
|
||||
'X-Auth-Token': self.keystone_token_id}
|
||||
r = self.get('/auth/tokens', headers=headers)
|
||||
self.assertValidTokenResponse(r, self.user)
|
||||
|
||||
r = self.delete('/projects/%(project_id)s' % {
|
||||
'project_id': self.project_id})
|
||||
|
||||
headers = {'X-Subject-Token': self.keystone_token_id,
|
||||
'X-Auth-Token': self.keystone_token_id}
|
||||
self.admin_request(path='/auth/tokens', headers=headers,
|
||||
method='GET', expected_status=404)
|
||||
|
||||
def test_token_chaining_is_not_allowed(self):
|
||||
self.test_oauth_flow()
|
||||
|
||||
#attempt to re-authenticate (token chain) with the given token
|
||||
path = '/v3/auth/tokens/'
|
||||
auth_data = self.build_authentication_request(
|
||||
token=self.keystone_token_id)
|
||||
|
||||
self.admin_request(
|
||||
path=path,
|
||||
body=auth_data,
|
||||
token=self.keystone_token_id,
|
||||
method='POST',
|
||||
expected_status=403)
|
||||
|
||||
def test_list_keystone_tokens_by_consumer(self):
|
||||
self.test_oauth_flow()
|
||||
tokens = self.token_api.list_tokens(self.user_id,
|
||||
consumer_id=self.consumer.key)
|
||||
keystone_token_uuid = cms.cms_hash_token(self.keystone_token_id)
|
||||
self.assertTrue(len(tokens) > 0)
|
||||
self.assertTrue(keystone_token_uuid in tokens)
|
||||
|
||||
|
||||
class MaliciousOAuth1Tests(OAuth1Tests):
|
||||
|
||||
def test_bad_consumer_secret(self):
|
||||
consumer = self._create_single_consumer()
|
||||
consumer_id = consumer.get('id')
|
||||
consumer = oauth1.Consumer(consumer_id, "bad_secret")
|
||||
url, headers = self._create_request_token(consumer,
|
||||
self.role_id,
|
||||
self.project_id)
|
||||
self.post(url, headers=headers, expected_status=500)
|
||||
|
||||
def test_bad_request_token_key(self):
|
||||
consumer = self._create_single_consumer()
|
||||
consumer_id = consumer.get('id')
|
||||
consumer_secret = consumer.get('secret')
|
||||
consumer = oauth1.Consumer(consumer_id, consumer_secret)
|
||||
url, headers = self._create_request_token(consumer,
|
||||
self.role_id,
|
||||
self.project_id)
|
||||
self.post(url, headers=headers)
|
||||
url = self._authorize_request_token("bad_key")
|
||||
self.put(url, expected_status=404)
|
||||
|
||||
def test_bad_verifier(self):
|
||||
consumer = self._create_single_consumer()
|
||||
consumer_id = consumer.get('id')
|
||||
consumer_secret = consumer.get('secret')
|
||||
consumer = oauth1.Consumer(consumer_id, consumer_secret)
|
||||
|
||||
url, headers = self._create_request_token(consumer,
|
||||
self.role_id,
|
||||
self.project_id)
|
||||
content = self.post(url, headers=headers)
|
||||
credentials = urlparse.parse_qs(content.result)
|
||||
request_key = credentials.get('oauth_token')[0]
|
||||
request_secret = credentials.get('oauth_token_secret')[0]
|
||||
request_token = oauth1.Token(request_key, request_secret)
|
||||
|
||||
url = self._authorize_request_token(request_key)
|
||||
resp = self.put(url, expected_status=200)
|
||||
verifier = resp.result['token']['oauth_verifier']
|
||||
self.assertIsNotNone(verifier)
|
||||
|
||||
request_token.set_verifier("bad verifier")
|
||||
url, headers = self._create_access_token(consumer,
|
||||
request_token)
|
||||
self.post(url, headers=headers, expected_status=401)
|
||||
|
||||
def test_bad_requested_roles(self):
|
||||
consumer = self._create_single_consumer()
|
||||
consumer_id = consumer.get('id')
|
||||
consumer_secret = consumer.get('secret')
|
||||
consumer = oauth1.Consumer(consumer_id, consumer_secret)
|
||||
|
||||
url, headers = self._create_request_token(consumer,
|
||||
"bad_role",
|
||||
self.project_id)
|
||||
self.post(url, headers=headers, expected_status=401)
|
||||
|
||||
def test_bad_authorizing_roles(self):
|
||||
consumer = self._create_single_consumer()
|
||||
consumer_id = consumer.get('id')
|
||||
consumer_secret = consumer.get('secret')
|
||||
consumer = oauth1.Consumer(consumer_id, consumer_secret)
|
||||
|
||||
url, headers = self._create_request_token(consumer,
|
||||
self.role_id,
|
||||
self.project_id)
|
||||
content = self.post(url, headers=headers)
|
||||
credentials = urlparse.parse_qs(content.result)
|
||||
request_key = credentials.get('oauth_token')[0]
|
||||
|
||||
self.identity_api.remove_role_from_user_and_project(self.user_id,
|
||||
self.project_id,
|
||||
self.role_id)
|
||||
url = self._authorize_request_token(request_key)
|
||||
self.admin_request(path=url, method='PUT', expected_status=404)
|
||||
|
||||
def test_expired_authorizing_request_token(self):
|
||||
CONF.oauth1.request_token_duration = -1
|
||||
|
||||
consumer = self._create_single_consumer()
|
||||
consumer_id = consumer.get('id')
|
||||
consumer_secret = consumer.get('secret')
|
||||
self.consumer = oauth1.Consumer(consumer_id, consumer_secret)
|
||||
self.assertIsNotNone(self.consumer.key)
|
||||
|
||||
url, headers = self._create_request_token(self.consumer,
|
||||
self.role_id,
|
||||
self.project_id)
|
||||
content = self.post(url, headers=headers)
|
||||
credentials = urlparse.parse_qs(content.result)
|
||||
request_key = credentials.get('oauth_token')[0]
|
||||
request_secret = credentials.get('oauth_token_secret')[0]
|
||||
self.request_token = oauth1.Token(request_key, request_secret)
|
||||
self.assertIsNotNone(self.request_token.key)
|
||||
|
||||
url = self._authorize_request_token(request_key)
|
||||
self.put(url, expected_status=401)
|
||||
|
||||
def test_expired_creating_keystone_token(self):
|
||||
CONF.oauth1.access_token_duration = -1
|
||||
consumer = self._create_single_consumer()
|
||||
consumer_id = consumer.get('id')
|
||||
consumer_secret = consumer.get('secret')
|
||||
self.consumer = oauth1.Consumer(consumer_id, consumer_secret)
|
||||
self.assertIsNotNone(self.consumer.key)
|
||||
|
||||
url, headers = self._create_request_token(self.consumer,
|
||||
self.role_id,
|
||||
self.project_id)
|
||||
content = self.post(url, headers=headers)
|
||||
credentials = urlparse.parse_qs(content.result)
|
||||
request_key = credentials.get('oauth_token')[0]
|
||||
request_secret = credentials.get('oauth_token_secret')[0]
|
||||
self.request_token = oauth1.Token(request_key, request_secret)
|
||||
self.assertIsNotNone(self.request_token.key)
|
||||
|
||||
url = self._authorize_request_token(request_key)
|
||||
resp = self.put(url, expected_status=200)
|
||||
self.verifier = resp.result['token']['oauth_verifier']
|
||||
|
||||
self.request_token.set_verifier(self.verifier)
|
||||
url, headers = self._create_access_token(self.consumer,
|
||||
self.request_token)
|
||||
content = self.post(url, headers=headers)
|
||||
credentials = urlparse.parse_qs(content.result)
|
||||
access_key = credentials.get('oauth_token')[0]
|
||||
access_secret = credentials.get('oauth_token_secret')[0]
|
||||
self.access_token = oauth1.Token(access_key, access_secret)
|
||||
self.assertIsNotNone(self.access_token.key)
|
||||
|
||||
url, headers, body = self._get_oauth_token(self.consumer,
|
||||
self.access_token)
|
||||
self.post(url, headers=headers, body=body, expected_status=401)
|
@ -90,6 +90,29 @@ class Token(kvs.Base, token.Driver):
|
||||
tokens.append(token.split('-', 1)[1])
|
||||
return tokens
|
||||
|
||||
def _consumer_matches(self, consumer_id, token_ref_dict):
|
||||
if consumer_id is None:
|
||||
return True
|
||||
else:
|
||||
if 'token_data' in token_ref_dict:
|
||||
token_data = token_ref_dict.get('token_data')
|
||||
if 'token' in token_data:
|
||||
token = token_data.get('token')
|
||||
oauth = token.get('OS-OAUTH1')
|
||||
if oauth and oauth.get('consumer_id') == consumer_id:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _list_tokens_for_consumer(self, consumer_id):
|
||||
tokens = []
|
||||
now = timeutils.utcnow()
|
||||
for token, ref in self.db.items():
|
||||
if not token.startswith('token-') or self.is_expired(now, ref):
|
||||
continue
|
||||
if self._consumer_matches(consumer_id, ref):
|
||||
tokens.append(token.split('-', 1)[1])
|
||||
return tokens
|
||||
|
||||
def _list_tokens_for_user(self, user_id, tenant_id=None):
|
||||
def user_matches(user_id, ref):
|
||||
return ref.get('user') and ref['user'].get('id') == user_id
|
||||
@ -110,9 +133,12 @@ class Token(kvs.Base, token.Driver):
|
||||
tokens.append(token.split('-', 1)[1])
|
||||
return tokens
|
||||
|
||||
def list_tokens(self, user_id, tenant_id=None, trust_id=None):
|
||||
def list_tokens(self, user_id, tenant_id=None, trust_id=None,
|
||||
consumer_id=None):
|
||||
if trust_id:
|
||||
return self._list_tokens_for_trust(trust_id)
|
||||
if consumer_id:
|
||||
return self._list_tokens_for_consumer(consumer_id)
|
||||
else:
|
||||
return self._list_tokens_for_user(user_id, tenant_id)
|
||||
|
||||
|
@ -178,7 +178,8 @@ class Token(token.Driver):
|
||||
self._add_to_revocation_list(data)
|
||||
return result
|
||||
|
||||
def list_tokens(self, user_id, tenant_id=None, trust_id=None):
|
||||
def list_tokens(self, user_id, tenant_id=None, trust_id=None,
|
||||
consumer_id=None):
|
||||
tokens = []
|
||||
user_key = self._prefix_user_id(user_id)
|
||||
user_record = self.client.get(user_key) or ""
|
||||
@ -199,6 +200,13 @@ class Token(token.Driver):
|
||||
continue
|
||||
if trust != trust_id:
|
||||
continue
|
||||
if consumer_id is not None:
|
||||
try:
|
||||
oauth = token_ref['token_data']['token']['OS-OAUTH1']
|
||||
if oauth.get('consumer_id') != consumer_id:
|
||||
continue
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
tokens.append(token_id)
|
||||
return tokens
|
||||
|
@ -78,7 +78,8 @@ class Token(sql.Base, token.Driver):
|
||||
token_ref.valid = False
|
||||
session.flush()
|
||||
|
||||
def delete_tokens(self, user_id, tenant_id=None, trust_id=None):
|
||||
def delete_tokens(self, user_id, tenant_id=None, trust_id=None,
|
||||
consumer_id=None):
|
||||
"""Deletes all tokens in one session
|
||||
|
||||
The user_id will be ignored if the trust_id is specified. user_id
|
||||
@ -103,6 +104,11 @@ class Token(sql.Base, token.Driver):
|
||||
token_ref_dict = token_ref.to_dict()
|
||||
if not self._tenant_matches(tenant_id, token_ref_dict):
|
||||
continue
|
||||
if consumer_id:
|
||||
token_ref_dict = token_ref.to_dict()
|
||||
if not self._consumer_matches(consumer_id, token_ref_dict):
|
||||
continue
|
||||
|
||||
token_ref.valid = False
|
||||
|
||||
session.flush()
|
||||
@ -112,6 +118,13 @@ class Token(sql.Base, token.Driver):
|
||||
(token_ref_dict.get('tenant') and
|
||||
token_ref_dict['tenant'].get('id') == tenant_id))
|
||||
|
||||
def _consumer_matches(self, consumer_id, token_ref_dict):
|
||||
if consumer_id is None:
|
||||
return True
|
||||
else:
|
||||
oauth = token_ref_dict['token_data']['token'].get('OS-OAUTH1', {})
|
||||
return oauth and oauth['consumer_id'] == consumer_id
|
||||
|
||||
def _list_tokens_for_trust(self, trust_id):
|
||||
session = self.get_session()
|
||||
tokens = []
|
||||
@ -141,9 +154,29 @@ class Token(sql.Base, token.Driver):
|
||||
tokens.append(token_ref['id'])
|
||||
return tokens
|
||||
|
||||
def list_tokens(self, user_id, tenant_id=None, trust_id=None):
|
||||
def _list_tokens_for_consumer(self, user_id, consumer_id):
|
||||
tokens = []
|
||||
session = self.get_session()
|
||||
with session.begin():
|
||||
now = timeutils.utcnow()
|
||||
query = session.query(TokenModel)
|
||||
query = query.filter(TokenModel.expires > now)
|
||||
query = query.filter(TokenModel.user_id == user_id)
|
||||
token_references = query.filter_by(valid=True)
|
||||
|
||||
for token_ref in token_references:
|
||||
token_ref_dict = token_ref.to_dict()
|
||||
if self._consumer_matches(consumer_id, token_ref_dict):
|
||||
tokens.append(token_ref_dict['id'])
|
||||
session.flush()
|
||||
return tokens
|
||||
|
||||
def list_tokens(self, user_id, tenant_id=None, trust_id=None,
|
||||
consumer_id=None):
|
||||
if trust_id:
|
||||
return self._list_tokens_for_trust(trust_id)
|
||||
if consumer_id:
|
||||
return self._list_tokens_for_consumer(user_id, consumer_id)
|
||||
else:
|
||||
return self._list_tokens_for_user(user_id, tenant_id)
|
||||
|
||||
|
@ -174,41 +174,51 @@ class Driver(object):
|
||||
"""
|
||||
raise exception.NotImplemented()
|
||||
|
||||
def delete_tokens(self, user_id, tenant_id=None, trust_id=None):
|
||||
def delete_tokens(self, user_id, tenant_id=None, trust_id=None,
|
||||
consumer_id=None):
|
||||
"""Deletes tokens by user.
|
||||
If the tenant_id is not None, only delete the tokens by user id under
|
||||
the specified tenant.
|
||||
If the trust_id is not None, it will be used to query tokens and the
|
||||
user_id will be ignored.
|
||||
If the consumer_id is not None, only delete the tokens by consumer id
|
||||
that match the specified consumer id
|
||||
|
||||
:param user_id: identity of user
|
||||
:type user_id: string
|
||||
:param tenant_id: identity of the tenant
|
||||
:type tenant_id: string
|
||||
:param trust_id: identified of the trust
|
||||
:param trust_id: identity of the trust
|
||||
:type trust_id: string
|
||||
:param consumer_id: identity of the consumer
|
||||
:type consumer_id: string
|
||||
:returns: None.
|
||||
:raises: keystone.exception.TokenNotFound
|
||||
|
||||
"""
|
||||
token_list = self.list_tokens(user_id,
|
||||
tenant_id=tenant_id,
|
||||
trust_id=trust_id)
|
||||
trust_id=trust_id,
|
||||
consumer_id=consumer_id)
|
||||
|
||||
for token in token_list:
|
||||
try:
|
||||
self.delete_token(token)
|
||||
except exception.NotFound:
|
||||
pass
|
||||
|
||||
def list_tokens(self, user_id, tenant_id=None, trust_id=None):
|
||||
def list_tokens(self, user_id, tenant_id=None, trust_id=None,
|
||||
consumer_id=None):
|
||||
"""Returns a list of current token_id's for a user
|
||||
|
||||
:param user_id: identity of the user
|
||||
:type user_id: string
|
||||
:param tenant_id: identity of the tenant
|
||||
:type tenant_id: string
|
||||
:param trust_id: identified of the trust
|
||||
:param trust_id: identity of the trust
|
||||
:type trust_id: string
|
||||
:param consumer_id: identity of the consumer
|
||||
:type consumer_id: string
|
||||
:returns: list of token_id's
|
||||
|
||||
"""
|
||||
|
@ -18,6 +18,7 @@
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import json
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
@ -206,12 +207,23 @@ class V3TokenDataHelper(object):
|
||||
'domain': self._get_filtered_domain(user_ref['domain_id'])}
|
||||
token_data['user'] = filtered_user
|
||||
|
||||
def _populate_oauth_section(self, token_data, access_token):
|
||||
if access_token:
|
||||
access_token_id = access_token['id']
|
||||
consumer_id = access_token['consumer_id']
|
||||
token_data['OS-OAUTH1'] = ({'access_token_id': access_token_id,
|
||||
'consumer_id': consumer_id})
|
||||
|
||||
def _populate_roles(self, token_data, user_id, domain_id, project_id,
|
||||
trust):
|
||||
trust, access_token):
|
||||
if 'roles' in token_data:
|
||||
# no need to repopulate roles
|
||||
return
|
||||
|
||||
if access_token:
|
||||
token_data['roles'] = json.loads(access_token['requested_roles'])
|
||||
return
|
||||
|
||||
if CONF.trust.enabled and trust:
|
||||
token_user_id = trust['trustor_user_id']
|
||||
token_project_id = trust['project_id']
|
||||
@ -288,7 +300,7 @@ class V3TokenDataHelper(object):
|
||||
def get_token_data(self, user_id, method_names, extras,
|
||||
domain_id=None, project_id=None, expires=None,
|
||||
trust=None, token=None, include_catalog=True,
|
||||
bind=None):
|
||||
bind=None, access_token=None):
|
||||
token_data = {'methods': method_names,
|
||||
'extras': extras}
|
||||
|
||||
@ -307,15 +319,17 @@ class V3TokenDataHelper(object):
|
||||
|
||||
self._populate_scope(token_data, domain_id, project_id)
|
||||
self._populate_user(token_data, user_id, domain_id, project_id, trust)
|
||||
self._populate_roles(token_data, user_id, domain_id, project_id, trust)
|
||||
self._populate_roles(token_data, user_id, domain_id, project_id, trust,
|
||||
access_token)
|
||||
if include_catalog:
|
||||
self._populate_service_catalog(token_data, user_id, domain_id,
|
||||
project_id, trust)
|
||||
self._populate_token_dates(token_data, expires=expires, trust=trust)
|
||||
self._populate_oauth_section(token_data, access_token)
|
||||
return {'token': token_data}
|
||||
|
||||
|
||||
@dependency.requires('token_api', 'identity_api', 'catalog_api')
|
||||
@dependency.requires('token_api', 'identity_api', 'catalog_api', 'oauth_api')
|
||||
class Provider(token.provider.Provider):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Provider, self).__init__(*args, **kwargs)
|
||||
@ -380,6 +394,12 @@ class Provider(token.provider.Provider):
|
||||
if (CONF.trust.enabled and not trust and metadata_ref and
|
||||
'trust_id' in metadata_ref):
|
||||
trust = self.trust_api.get_trust(metadata_ref['trust_id'])
|
||||
|
||||
access_token = None
|
||||
if 'oauth1' in method_names:
|
||||
access_token_id = auth_context['access_token_id']
|
||||
access_token = self.oauth_api.get_access_token(access_token_id)
|
||||
|
||||
token_data = self.v3_token_data_helper.get_token_data(
|
||||
user_id,
|
||||
method_names,
|
||||
@ -389,7 +409,8 @@ class Provider(token.provider.Provider):
|
||||
expires=expires_at,
|
||||
trust=trust,
|
||||
bind=auth_context.get('bind') if auth_context else None,
|
||||
include_catalog=include_catalog)
|
||||
include_catalog=include_catalog,
|
||||
access_token=access_token)
|
||||
|
||||
token_id = self._get_token_id(token_data)
|
||||
try:
|
||||
|
@ -17,3 +17,4 @@ iso8601>=0.1.4
|
||||
python-keystoneclient>=0.3.0
|
||||
oslo.config>=1.1.0
|
||||
Babel>=0.9.6
|
||||
oauth2
|
||||
|
Loading…
Reference in New Issue
Block a user