Add request/access token and consumer support for keystoneclient

Add support for creating request and access tokens,
and to authorize request tokens. Also adding basic CRUD for
consumer entities.

implements: bp add-oauth-support

Change-Id: I9137e3426c82c73855ae0e50317cfd6477195318
This commit is contained in:
Steve Martinelli
2013-12-09 07:50:44 -06:00
parent f12afc6c73
commit 2e7bdb872e
8 changed files with 390 additions and 0 deletions

View File

@@ -0,0 +1,176 @@
# 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 uuid
import httpretty
import oauthlib.oauth1 as oauth1
from keystoneclient.tests.v3 import utils
from keystoneclient.v3.contrib.oauth1 import access_tokens
from keystoneclient.v3.contrib.oauth1 import consumers
from keystoneclient.v3.contrib.oauth1 import request_tokens
class ConsumerTests(utils.TestCase, utils.CrudTests):
def setUp(self):
super(ConsumerTests, self).setUp()
self.key = 'consumer'
self.collection_key = 'consumers'
self.model = consumers.Consumer
self.manager = self.client.oauth1.consumers
self.path_prefix = 'OS-OAUTH1'
def new_ref(self, **kwargs):
kwargs = super(ConsumerTests, self).new_ref(**kwargs)
kwargs.setdefault('description', uuid.uuid4().hex)
return kwargs
@httpretty.activate
def test_description_is_optional(self, **kwargs):
consumer_id = uuid.uuid4().hex
resp_ref = {'consumer': {'description': None,
'id': consumer_id}}
self.stub_url(httpretty.POST,
[self.path_prefix, self.collection_key],
status=200, json=resp_ref)
consumer = self.manager.create()
self.assertIsNone(consumer.description)
class TokenTests(utils.TestCase):
def _new_oauth_token(self):
key = uuid.uuid4().hex
secret = uuid.uuid4().hex
token = 'oauth_token=%s&oauth_token_secret=%s' % (key, secret)
return (key, secret, token)
def _new_oauth_token_with_expires_at(self):
key = uuid.uuid4().hex
secret = uuid.uuid4().hex
expires_at = uuid.uuid4().hex
token = ('oauth_token=%s&oauth_token_secret=%s'
'&oauth_expires_at=%s' % (key, secret, expires_at))
return (key, secret, expires_at, token)
def _validate_oauth_headers(self, auth_header, oc):
self.assertTrue(auth_header.startswith('OAuth '))
auth_header = auth_header[6:]
header_params = oc.get_oauth_params()
parameters = dict(header_params)
self.assertEqual(parameters['oauth_signature_method'], 'HMAC-SHA1')
self.assertEqual(parameters['oauth_version'], '1.0')
self.assertIsNotNone(parameters['oauth_nonce'])
self.assertIsNotNone(parameters['oauth_timestamp'])
self.assertEqual(parameters['oauth_consumer_key'], oc.client_key)
if oc.resource_owner_key:
self.assertEqual(parameters['oauth_token'], oc.resource_owner_key)
if oc.verifier:
self.assertEqual(parameters['oauth_verifier'], oc.verifier)
return parameters
class RequestTokenTests(TokenTests):
def setUp(self):
super(RequestTokenTests, self).setUp()
self.model = request_tokens.RequestToken
self.manager = self.client.oauth1.request_tokens
self.path_prefix = 'OS-OAUTH1'
@httpretty.activate
def test_authorize_request_token(self):
request_key = uuid.uuid4().hex
verifier = uuid.uuid4().hex
resp_ref = {'token': {'oauth_verifier': verifier}}
role_ids = uuid.uuid4().hex
exp_body = {'roles': [{'id': role_ids}]}
self.stub_url(httpretty.PUT,
[self.path_prefix, 'authorize', request_key],
status=200, json=resp_ref)
# Assert the manager is returning the expected data
token = self.manager.authorize(request_key, [role_ids])
self.assertEqual(token.oauth_verifier, verifier)
# Assert that the request was sent in the expected structure
self.assertRequestBodyIs(json=exp_body)
@httpretty.activate
def test_create_request_token(self):
project_id = uuid.uuid4().hex
consumer_key = uuid.uuid4().hex
consumer_secret = uuid.uuid4().hex
request_key, request_secret, resp_ref = self._new_oauth_token()
self.stub_url(httpretty.POST, [self.path_prefix, 'request_token'],
status=201, json=resp_ref)
# Assert the manager is returning request token object
request_token = self.manager.create(consumer_key, consumer_secret,
project_id)
self.assertIsInstance(request_token, self.model)
self.assertEqual(request_token.request_key, request_key)
self.assertEqual(request_token.request_secret, request_secret)
# Assert that the project id is in the header
self.assertRequestHeaderEqual('requested_project_id', project_id)
req_headers = httpretty.last_request().headers
# Assert that the headers have the same oauthlib data
oc = oauth1.Client(consumer_key, client_secret=consumer_secret,
signature_method=oauth1.SIGNATURE_HMAC,
callback_uri="oob")
self._validate_oauth_headers(req_headers['Authorization'], oc)
class AccessTokenTests(TokenTests):
def setUp(self):
super(AccessTokenTests, self).setUp()
self.manager = self.client.oauth1.access_tokens
self.model = access_tokens.AccessToken
self.path_prefix = 'OS-OAUTH1'
@httpretty.activate
def test_create_access_token_expires_at(self):
verifier = uuid.uuid4().hex
consumer_key = uuid.uuid4().hex
consumer_secret = uuid.uuid4().hex
request_key = uuid.uuid4().hex
request_secret = uuid.uuid4().hex
t = self._new_oauth_token_with_expires_at()
access_key, access_secret, expires_at, resp_ref = t
self.stub_url(httpretty.POST, [self.path_prefix, 'access_token'],
status=201, json=resp_ref)
# Assert that the manager creates an access token object
access_token = self.manager.create(consumer_key, consumer_secret,
request_key, request_secret,
verifier)
self.assertIsInstance(access_token, self.model)
self.assertEqual(access_token.access_key, access_key)
self.assertEqual(access_token.access_secret, access_secret)
self.assertEqual(access_token.expires, expires_at)
# Assert that the headers have the same oauthlib data
req_headers = httpretty.last_request().headers
oc = oauth1.Client(consumer_key, client_secret=consumer_secret,
resource_owner_key=request_key,
resource_owner_secret=request_secret,
signature_method=oauth1.SIGNATURE_HMAC,
verifier=verifier)
self._validate_oauth_headers(req_headers['Authorization'], oc)

View File

@@ -19,6 +19,7 @@ from keystoneclient.auth.identity import v3 as v3_auth
from keystoneclient import exceptions from keystoneclient import exceptions
from keystoneclient import httpclient from keystoneclient import httpclient
from keystoneclient.openstack.common import jsonutils from keystoneclient.openstack.common import jsonutils
from keystoneclient.v3.contrib import oauth1
from keystoneclient.v3.contrib import trusts from keystoneclient.v3.contrib import trusts
from keystoneclient.v3 import credentials from keystoneclient.v3 import credentials
from keystoneclient.v3 import domains from keystoneclient.v3 import domains
@@ -96,6 +97,7 @@ class Client(httpclient.HTTPClient):
self.endpoints = endpoints.EndpointManager(self) self.endpoints = endpoints.EndpointManager(self)
self.domains = domains.DomainManager(self) self.domains = domains.DomainManager(self)
self.groups = groups.GroupManager(self) self.groups = groups.GroupManager(self)
self.oauth1 = oauth1.OAuthManager(self)
self.policies = policies.PolicyManager(self) self.policies = policies.PolicyManager(self)
self.projects = projects.ProjectManager(self) self.projects = projects.ProjectManager(self)
self.roles = roles.RoleManager(self) self.roles = roles.RoleManager(self)

View File

@@ -0,0 +1,13 @@
# 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 keystoneclient.v3.contrib.oauth1.core import * # flake8: noqa

View File

@@ -0,0 +1,59 @@
# 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 __future__ import unicode_literals
import oauthlib.oauth1 as oauth1
from six.moves.urllib import parse as urlparse
from keystoneclient import base
class AccessToken(base.Resource):
pass
class AccessTokenManager(base.CrudManager):
"""Manager class for manipulating Identity OAuth Access Tokens."""
resource_class = AccessToken
def create(self, consumer_key, consumer_secret, request_key,
request_secret, verifier):
# sign the request using oauthlib
endpoint = '/OS-OAUTH1/access_token'
oauth_client = oauth1.Client(consumer_key,
client_secret=consumer_secret,
resource_owner_key=request_key,
resource_owner_secret=request_secret,
signature_method=oauth1.SIGNATURE_HMAC,
verifier=verifier)
url = self.client.auth_url + endpoint
url, headers, body = oauth_client.sign(url, http_method='POST')
# actually send the request
resp, body = self.client.post(endpoint, headers=headers)
# returned format will be:
# 'oauth_token=12345&oauth_token_secret=67890'
# with 'oauth_expires_at' possibly there, too
credentials = urlparse.parse_qs(body)
key = credentials['oauth_token'][0]
secret = credentials['oauth_token_secret'][0]
access_token = {'access_key': key, 'id': key,
'access_secret': secret}
if credentials.get('oauth_expires_at'):
expires = credentials['oauth_expires_at'][0]
access_token['expires'] = expires
return self.resource_class(self, access_token)

View File

@@ -0,0 +1,51 @@
# 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 keystoneclient import base
class Consumer(base.Resource):
"""Represents an OAuth Consumer.
Attributes:
* id: a uuid that identifies the consumer
* description: a short description of the consumer
"""
pass
class ConsumerManager(base.CrudManager):
"""Manager class for manipulating Identity Consumers."""
resource_class = Consumer
collection_key = 'consumers'
key = 'consumer'
base_url = '/OS-OAUTH1'
def create(self, description=None, **kwargs):
return super(ConsumerManager, self).create(
description=description,
**kwargs)
def get(self, consumer):
return super(ConsumerManager, self).get(
consumer_id=base.getid(consumer))
def update(self, consumer, description=None, **kwargs):
return super(ConsumerManager, self).update(
consumer_id=base.getid(consumer),
description=description,
**kwargs)
def delete(self, consumer):
return super(ConsumerManager, self).delete(
consumer_id=base.getid(consumer))

View File

@@ -0,0 +1,23 @@
# 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 keystoneclient.v3.contrib.oauth1 import access_tokens
from keystoneclient.v3.contrib.oauth1 import consumers
from keystoneclient.v3.contrib.oauth1 import request_tokens
class OAuthManager(object):
def __init__(self, api):
self.access_tokens = access_tokens.AccessTokenManager(api)
self.consumers = consumers.ConsumerManager(api)
self.request_tokens = request_tokens.RequestTokenManager(api)

View File

@@ -0,0 +1,65 @@
# 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 __future__ import unicode_literals
import oauthlib.oauth1 as oauth1
from six.moves.urllib import parse as urlparse
from keystoneclient import base
class RequestToken(base.Resource):
def authorize(self, roles):
self.manager.authorize(self, roles)
class RequestTokenManager(base.CrudManager):
"""Manager class for manipulating Identity OAuth Request Tokens."""
resource_class = RequestToken
def authorize(self, request_token, roles):
request_id = base.getid(request_token)
endpoint = '/OS-OAUTH1/authorize/%s' % (request_id)
body = {'roles': [{'id': base.getid(r_id)} for r_id in roles]}
return self._put(endpoint, body, "token")
def create(self, consumer_key, consumer_secret, project):
# sign the request using oauthlib
endpoint = '/OS-OAUTH1/request_token'
headers = {'requested_project_id': base.getid(project)}
oauth_client = oauth1.Client(consumer_key,
client_secret=consumer_secret,
signature_method=oauth1.SIGNATURE_HMAC,
callback_uri="oob")
url = self.client.auth_url + endpoint
url, headers, body = oauth_client.sign(url, http_method='POST',
headers=headers)
# actually send the request
resp, body = self.client.post(endpoint, headers=headers)
# returned format will be:
# 'oauth_token=12345&oauth_token_secret=67890'
# with 'oauth_expires_at' possibly there, too
credentials = urlparse.parse_qs(body)
key = credentials.get('oauth_token')[0]
secret = credentials.get('oauth_token_secret')[0]
request_token = {'request_key': key, 'id': key,
'request_secret': secret}
if credentials.get('oauth_expires_at'):
expires = credentials.get('oauth_expires_at')[0]
request_token['expires'] = expires
return self.resource_class(self, request_token)

View File

@@ -7,3 +7,4 @@ pbr>=0.6,<1.0
PrettyTable>=0.7,<0.8 PrettyTable>=0.7,<0.8
requests>=1.1 requests>=1.1
six>=1.5.2 six>=1.5.2
oauthlib>=0.6