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:
176
keystoneclient/tests/v3/test_oauth1.py
Normal file
176
keystoneclient/tests/v3/test_oauth1.py
Normal 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)
|
@@ -19,6 +19,7 @@ from keystoneclient.auth.identity import v3 as v3_auth
|
||||
from keystoneclient import exceptions
|
||||
from keystoneclient import httpclient
|
||||
from keystoneclient.openstack.common import jsonutils
|
||||
from keystoneclient.v3.contrib import oauth1
|
||||
from keystoneclient.v3.contrib import trusts
|
||||
from keystoneclient.v3 import credentials
|
||||
from keystoneclient.v3 import domains
|
||||
@@ -96,6 +97,7 @@ class Client(httpclient.HTTPClient):
|
||||
self.endpoints = endpoints.EndpointManager(self)
|
||||
self.domains = domains.DomainManager(self)
|
||||
self.groups = groups.GroupManager(self)
|
||||
self.oauth1 = oauth1.OAuthManager(self)
|
||||
self.policies = policies.PolicyManager(self)
|
||||
self.projects = projects.ProjectManager(self)
|
||||
self.roles = roles.RoleManager(self)
|
||||
|
13
keystoneclient/v3/contrib/oauth1/__init__.py
Normal file
13
keystoneclient/v3/contrib/oauth1/__init__.py
Normal 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
|
59
keystoneclient/v3/contrib/oauth1/access_tokens.py
Normal file
59
keystoneclient/v3/contrib/oauth1/access_tokens.py
Normal 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)
|
51
keystoneclient/v3/contrib/oauth1/consumers.py
Normal file
51
keystoneclient/v3/contrib/oauth1/consumers.py
Normal 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))
|
23
keystoneclient/v3/contrib/oauth1/core.py
Normal file
23
keystoneclient/v3/contrib/oauth1/core.py
Normal 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)
|
65
keystoneclient/v3/contrib/oauth1/request_tokens.py
Normal file
65
keystoneclient/v3/contrib/oauth1/request_tokens.py
Normal 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)
|
@@ -7,3 +7,4 @@ pbr>=0.6,<1.0
|
||||
PrettyTable>=0.7,<0.8
|
||||
requests>=1.1
|
||||
six>=1.5.2
|
||||
oauthlib>=0.6
|
||||
|
Reference in New Issue
Block a user