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:
Steve Martinelli 2013-03-20 20:02:18 -07:00 committed by Dolph Mathews
parent 81534a182a
commit bcaa3072f3
30 changed files with 2311 additions and 19 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()

View 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']

View File

@ -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)

View File

@ -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')],

View 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

View 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.

View 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))

View 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()

View 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

View 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()

View 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.

View 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

View File

@ -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()

View 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.

View 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']))

View File

@ -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(),

View File

@ -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' %

View File

@ -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)

View File

@ -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

View File

@ -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')

View 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)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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
"""

View File

@ -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:

View File

@ -17,3 +17,4 @@ iso8601>=0.1.4
python-keystoneclient>=0.3.0
oslo.config>=1.1.0
Babel>=0.9.6
oauth2