From 01735608644a8b71d98f7798ccd6549e4db5de48 Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Wed, 30 Jan 2013 18:07:29 +0100 Subject: [PATCH] Implements token expiration handling This implements handling of token expiration. Once the token is expired, this will request automatically for a new one. A special case is introduced if the user specified a token when the client is initialized: this is the auth_token_from_user. In this case, we can't know the expiration date, so we just assume it will never expire and don't handle it ourself. Change-Id: I3771ff5d669da015d4aa259de422c5d81aed3eb4 Signed-off-by: Julien Danjou --- keystoneclient/client.py | 32 ++++++++++++++++++++---- keystoneclient/v2_0/client.py | 1 - tests/v2_0/test_auth.py | 47 ++++++++++++++++++++++++++++++++++- 3 files changed, 73 insertions(+), 7 deletions(-) diff --git a/keystoneclient/client.py b/keystoneclient/client.py index 77e8b3c92..fe4fb76f3 100644 --- a/keystoneclient/client.py +++ b/keystoneclient/client.py @@ -71,7 +71,6 @@ class HTTPClient(object): self.tenant_id = None self.tenant_name = None self.auth_url = None - self.auth_token = None self.management_url = None if timeout is not None: self.timeout = float(timeout) @@ -86,7 +85,6 @@ class HTTPClient(object): self.tenant_name = self.auth_ref.tenant_name self.auth_url = self.auth_ref.auth_url[0] self.management_url = self.auth_ref.management_url[0] - self.auth_token = self.auth_ref.auth_token # allow override of the auth_ref defaults from explicit # values provided to the client if username: @@ -98,7 +96,9 @@ class HTTPClient(object): if auth_url: self.auth_url = auth_url.rstrip('/') if token: - self.auth_token = token + self.auth_token_from_user = token + else: + self.auth_token_from_user = None if endpoint: self.management_url = endpoint.rstrip('/') self.password = password @@ -129,6 +129,23 @@ class HTTPClient(object): self.stale_duration = stale_duration or access.STALE_TOKEN_DURATION self.stale_duration = int(self.stale_duration) + @property + def auth_token(self): + if self.auth_token_from_user: + return self.auth_token_from_user + if self.auth_ref: + if self.auth_ref.will_expire_soon(self.stale_duration): + self.authenticate() + return self.auth_ref.auth_token + + @auth_token.setter + def auth_token(self, value): + self.auth_token_from_user = value + + @auth_token.deleter + def auth_token(selef): + del self.auth_token_from_user + def authenticate(self, username=None, password=None, tenant_name=None, tenant_id=None, auth_url=None, token=None): """ Authenticate user. @@ -168,7 +185,12 @@ class HTTPClient(object): password = password or self.password tenant_name = tenant_name or self.tenant_name tenant_id = tenant_id or self.tenant_id - token = token or self.auth_token + + if not token: + token = self.auth_token_from_user + if (not token and self.auth_ref + and not self.auth_ref.will_expire_soon(self.stale_duration)): + token = self.auth_ref.auth_token (keyring_key, auth_ref) = self.get_auth_ref_from_keyring(auth_url, username, @@ -227,7 +249,7 @@ class HTTPClient(object): keyring_key) if auth_ref: auth_ref = pickle.loads(auth_ref) - if auth_ref.will_expire_soon(self.stale_duration): + if self.auth_ref.will_expire_soon(self.stale_duration): # token has expired, don't use it auth_ref = None except Exception as e: diff --git a/keystoneclient/v2_0/client.py b/keystoneclient/v2_0/client.py index c378e3018..ab62111b3 100644 --- a/keystoneclient/v2_0/client.py +++ b/keystoneclient/v2_0/client.py @@ -147,7 +147,6 @@ class Client(client.HTTPClient): # if we got a response without a service catalog, set the local # list of tenants for introspection, and leave to client user # to determine what to do. Otherwise, load up the service catalog - self.auth_token = self.auth_ref.auth_token if self.auth_ref.scoped: if self.management_url is None and self.auth_ref.management_url: self.management_url = self.auth_ref.management_url[0] diff --git a/tests/v2_0/test_auth.py b/tests/v2_0/test_auth.py index cd9caa56d..0c0af9984 100644 --- a/tests/v2_0/test_auth.py +++ b/tests/v2_0/test_auth.py @@ -1,10 +1,12 @@ import copy +from datetime import timedelta import json import requests from keystoneclient.v2_0 import client from keystoneclient import exceptions +from keystoneclient.openstack.common import timeutils from tests import utils @@ -14,7 +16,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): self.TEST_RESPONSE_DICT = { "access": { "token": { - "expires": "12345", + "expires": "2999-01-01T00:00:10Z", "id": self.TEST_TOKEN, "tenant": { "id": self.TEST_TENANT_ID @@ -40,6 +42,49 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): 'User-Agent': 'python-keystoneclient', } + def test_authenticate_success_expired(self): + # Build an expired token + self.TEST_RESPONSE_DICT['access']['token']['expires'] = \ + (timeutils.utcnow() - timedelta(1)).isoformat() + resp = utils.TestResponse({ + "status_code": 200, + "text": json.dumps(self.TEST_RESPONSE_DICT), + }) + + kwargs = copy.copy(self.TEST_REQUEST_BASE) + kwargs['headers'] = self.TEST_REQUEST_HEADERS + kwargs['data'] = json.dumps(self.TEST_REQUEST_BODY) + requests.request('POST', + self.TEST_URL + "/tokens", + **kwargs).AndReturn((resp)) + self.mox.ReplayAll() + + cs = client.Client(tenant_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL, + username=self.TEST_USER, + password=self.TEST_TOKEN) + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["access"]["serviceCatalog"][3] + ['endpoints'][0]["adminURL"]) + + # Build a new response + self.mox.ResetAll() + TEST_TOKEN = "abcdef" + self.TEST_RESPONSE_DICT['access']['token']['expires'] = \ + "2999-01-01T00:00:10Z" + self.TEST_RESPONSE_DICT['access']['token']['id'] = TEST_TOKEN + + resp = utils.TestResponse({ + "status_code": 200, + "text": json.dumps(self.TEST_RESPONSE_DICT), + }) + requests.request('POST', + self.TEST_URL + "/tokens", + **kwargs).AndReturn((resp)) + self.mox.ReplayAll() + + self.assertEqual(cs.auth_token, TEST_TOKEN) + def test_authenticate_failure(self): _auth = 'auth' _cred = 'passwordCredentials'