diff options
| author | Jenkins <jenkins@review.openstack.org> | 2011-10-28 22:04:57 +0000 |
|---|---|---|
| committer | Gerrit Code Review <review@openstack.org> | 2011-10-28 22:04:57 +0000 |
| commit | b21d8e03c507f58aab32dce671cd9cdb6c136d6d (patch) | |
| tree | 1a337267311a5d34ae315015fe0bf82bca3f8a13 | |
| parent | 9655cde434deb633a761d2ece6324d2a3bbb60f9 (diff) | |
| parent | 8bd9225b4e0594259c59dc007a3f7d7de2591dc8 (diff) | |
Merge "Adding the concept of creating a Keystone HTTP client in Python which can be used in Keystone and imported from Keystone to allow for easier Keystone integration."
| -rw-r--r-- | keystone/client.py | 181 | ||||
| -rwxr-xr-x | keystone/common/exception.py | 4 | ||||
| -rw-r--r-- | keystone/test/functional/test_client.py | 88 |
3 files changed, 273 insertions, 0 deletions
diff --git a/keystone/client.py b/keystone/client.py new file mode 100644 index 00000000..e845c08e --- /dev/null +++ b/keystone/client.py @@ -0,0 +1,181 @@ +# Copyright (C) 2011 OpenStack LLC. +# +# 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. + +"""Python HTTP clients for accessing Keystone's Service and Admin APIs.""" + +import httplib +import json + +import keystone.common.exception + + +class ServiceClient(object): + """Keystone v2.0 HTTP API client for normal service function. + + Provides functionality for retrieving new tokens and for retrieving + a list of tenants which the supplied token has access to. + + """ + + _default_port = 5000 + + def __init__(self, host, port=None): + """Initialize client. + + :param host: The hostname or IP of the Keystone service to use + :param port: The port of the Keystone service to use + + """ + self.host = host + self.port = port or self._default_port + + def _http_request(self, verb, path, body=None, headers=None): + """Perform an HTTP request and return the HTTP response. + + :param verb: HTTP verb (e.g. GET, POST, etc.) + :param path: HTTP path (e.g. /v2.0/tokens) + :param body: HTTP Body content + :param headers: Dictionary of HTTP headers + :returns: httplib.HTTPResponse object + + """ + connection = httplib.HTTPConnection(self.auth_address) + connection.request(verb, path, body=body, headers=headers) + + response = connection.getresponse() + response.body = response.read() + status_int = int(response.status) + connection.close() + + if status_int < 200 or status_int >= 300: + msg = "Client received HTTP %d" % status_int + raise keystone.common.exception.ClientError(msg) + + return response + + @property + def auth_address(self): + """Return a host:port combination string.""" + return "%s:%d" % (self.host, self.port) + + def get_token(self, username, password): + """Retrieve a token from Keystone for a given user/password. + + :param username: The user name to authenticate with + :param password: The password to authenticate with + :returns: A string token + + """ + body = json.dumps({ + "auth": { + "passwordCredentials": { + "username": username, + "password": password, + }, + }, + }) + + headers = { + "Accept": "application/json", + "Content-Type": "application/json", + } + + response = self._http_request("POST", "/v2.0/tokens", body, headers) + token_id = json.loads(response.body)["access"]["token"]["id"] + + return token_id + + +class AdminClient(ServiceClient): + """Keystone v2.0 HTTP API client for administrative functions. + + Provides functionality for retrieving new tokens, validating existing + tokens, and retrieving user information from valid tokens. + + """ + + _default_port = 35357 + _default_admin_name = "admin" + _default_admin_pass = "password" + + def __init__(self, host, port=None, admin_name=None, admin_pass=None): + """Initialize client. + + :param host: The hostname or IP of the Keystone service to use + :param port: The port of the Keystone service to use + :param admin_name: The username to use for admin purposes + :param admin_pass: The password to use for the admin account + + """ + super(AdminClient, self).__init__(host, port=port) + self.admin_name = admin_name or self._default_admin_name + self.admin_pass = admin_pass or self._default_admin_pass + self._admin_token = None + + @property + def admin_token(self): + """Retrieve a valid admin token. + + If a token has already been retrieved, ensure that it is still valid + and then return it. If it has not already been retrieved or the token + is found to be invalid, retrieve a new token and return it. + + """ + token = self._admin_token + + if token is None or not self.check_token(token, token): + token = self.get_token(self.admin_name, self.admin_pass) + + self._admin_token = token + return self._admin_token + + def validate_token(self, token): + """Validate a token, returning details about the user. + + :param token: A token string + :returns: Object representing the user the token belongs to, or None + if the token is not valid. + + """ + url = "/v2.0/tokens/%s" % token + + headers = { + "Accept": "application/json", + "X-Auth-Token": self.admin_token, + } + + try: + response = self._http_request("GET", url, headers=headers) + except keystone.common.exception.ClientError: + return None + + return json.loads(response.body) + + def check_token(self, token, admin_token=None): + """Check to see if given token is valid. + + :param token: A token string + :param admin_token: The administrative token to use + :returns: True if token is valid, otherwise False + + """ + url = "/v2.0/tokens/%s" % token + headers = {"X-Auth-Token": admin_token or self.admin_token} + + try: + self._http_request("HEAD", url, headers=headers) + except keystone.common.exception.ClientError: + return False + + return True diff --git a/keystone/common/exception.py b/keystone/common/exception.py index ee4f4392..e734e94f 100755 --- a/keystone/common/exception.py +++ b/keystone/common/exception.py @@ -81,6 +81,10 @@ class DatabaseMigrationError(Error): pass +class ClientError(Error): + pass + + def wrap_exception(f): def _wrap(*args, **kw): try: diff --git a/keystone/test/functional/test_client.py b/keystone/test/functional/test_client.py new file mode 100644 index 00000000..635b7f0e --- /dev/null +++ b/keystone/test/functional/test_client.py @@ -0,0 +1,88 @@ +import unittest + +import keystone.common.exception +import keystone.client + + +class TestAdminClient(unittest.TestCase): + """ + Quick functional tests for the Keystone HTTP admin client. + """ + + def setUp(self): + """ + Run before each test. + """ + self.client = keystone.client.AdminClient("127.0.0.1", + admin_name="admin", + admin_pass="secrete") + + def test_admin_validate_token(self): + """ + Test that our admin token is valid. (HTTP GET) + """ + token = self.client.admin_token + result = self.client.validate_token(token) + self.assertEquals("admin", + result["access"]["user"]["username"]) + + def test_admin_check_token(self): + """ + Test that our admin token is valid. (HTTP HEAD) + """ + token = self.client.admin_token + self.assertTrue(self.client.check_token(token)) + + def test_admin_validate_token_fail(self): + """ + Test that validating an invalid token results in None. (HTTP GET) + """ + token = "bad_token" + self.assertTrue(self.client.validate_token(token) is None) + + def test_admin_check_token_fail(self): + """ + Test that checking an invalid token results in False. (HTTP HEAD) + """ + token = "bad_token" + self.assertFalse(self.client.check_token(token)) + + def test_admin_get_token(self): + """ + Test that we can generate a token given correct credentials. + """ + token = self.client.get_token("admin", "secrete") + self.assertEquals(self.client.admin_token, token) + + def test_admin_get_token_bad_auth(self): + """ + Test incorrect credentials generates a client error. + """ + with self.assertRaises(keystone.common.exception.ClientError): + token = self.client.get_token("bad_user", "bad_pass") + + +class TestServiceClient(unittest.TestCase): + """ + Quick functional tests for the Keystone HTTP service client. + """ + + def setUp(self): + """ + Run before each test. + """ + self.client = keystone.client.ServiceClient("127.0.0.1") + + def test_admin_get_token(self): + """ + Test that we can generate a token given correct credentials. + """ + token = self.client.get_token("admin", "secrete") + self.assertTrue(36, len(token)) + + def test_admin_get_token_bad_auth(self): + """ + Test incorrect credentials generates a client error. + """ + with self.assertRaises(keystone.common.exception.ClientError): + token = self.client.get_token("bad_user", "bad_pass") |
