Merge "Allow saving and caching the plugin auth state"
This commit is contained in:
@@ -11,11 +11,15 @@
|
||||
# under the License.
|
||||
|
||||
import abc
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import threading
|
||||
|
||||
import six
|
||||
|
||||
from keystoneauth1 import _utils as utils
|
||||
from keystoneauth1 import access
|
||||
from keystoneauth1 import discover
|
||||
from keystoneauth1 import exceptions
|
||||
from keystoneauth1 import plugin
|
||||
@@ -305,3 +309,82 @@ class BaseIdentityPlugin(plugin.BaseAuthPlugin):
|
||||
session_endpoint_cache[url] = disc
|
||||
|
||||
return disc
|
||||
|
||||
def get_cache_id_elements(self):
|
||||
"""Get the elements for this auth plugin that make it unique.
|
||||
|
||||
As part of the get_cache_id requirement we need to determine what
|
||||
aspects of this plugin and its values that make up the unique elements.
|
||||
|
||||
This should be overriden by plugins that wish to allow caching.
|
||||
|
||||
:returns: The unique attributes and values of this plugin.
|
||||
:rtype: A flat dict with a str key and str or None value. This is
|
||||
required as we feed these values into a hash. Pairs where the
|
||||
value is None are ignored in the hashed id.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_cache_id(self):
|
||||
"""Fetch an identifier that uniquely identifies the auth options.
|
||||
|
||||
The returned identifier need not be decomposable or otherwise provide
|
||||
any way to recreate the plugin.
|
||||
|
||||
This string MUST change if any of the parameters that are used to
|
||||
uniquely identity this plugin change. It should not change upon a
|
||||
reauthentication of the plugin.
|
||||
|
||||
:returns: A unique string for the set of options
|
||||
:rtype: str or None if this is unsupported or unavailable.
|
||||
"""
|
||||
try:
|
||||
elements = self.get_cache_id_elements()
|
||||
except NotImplementedError:
|
||||
return None
|
||||
|
||||
hasher = hashlib.sha256()
|
||||
|
||||
for k, v in sorted(six.iteritems(elements)):
|
||||
if v is not None:
|
||||
# NOTE(jamielennox): in python3 you need to pass bytes to hash
|
||||
if isinstance(k, six.string_types):
|
||||
k = k.encode('utf-8')
|
||||
if isinstance(v, six.string_types):
|
||||
v = v.encode('utf-8')
|
||||
|
||||
hasher.update(k)
|
||||
hasher.update(v)
|
||||
|
||||
return base64.b64encode(hasher.digest()).decode('utf-8')
|
||||
|
||||
def get_auth_state(self):
|
||||
"""Retrieve the current authentication state for the plugin.
|
||||
|
||||
Retrieve any internal state that represents the authenticated plugin.
|
||||
|
||||
This should not fetch any new data if it is not present.
|
||||
|
||||
:returns: a string that can be stored or None if there is no auth state
|
||||
present in the plugin. This string can be reloaded with
|
||||
set_auth_state to set the same authentication.
|
||||
:rtype: str or None if no auth present.
|
||||
"""
|
||||
if self.auth_ref:
|
||||
data = {'auth_token': self.auth_ref.auth_token,
|
||||
'body': self.auth_ref._data}
|
||||
|
||||
return json.dumps(data)
|
||||
|
||||
def set_auth_state(self, data):
|
||||
"""Install existing authentication state for a plugin.
|
||||
|
||||
Take the output of get_auth_state and install that authentication state
|
||||
into the current authentication plugin.
|
||||
"""
|
||||
if data:
|
||||
auth_data = json.loads(data)
|
||||
self.auth_ref = access.create(body=auth_data['body'],
|
||||
auth_token=auth_data['auth_token'])
|
||||
else:
|
||||
self.auth_ref = None
|
||||
|
||||
@@ -138,6 +138,15 @@ class Password(Auth):
|
||||
|
||||
return {'passwordCredentials': auth}
|
||||
|
||||
def get_cache_id_elements(self):
|
||||
return {'username': self.username,
|
||||
'user_id': self.user_id,
|
||||
'password': self.password,
|
||||
'auth_url': self.auth_url,
|
||||
'tenant_id': self.tenant_id,
|
||||
'tenant_name': self.tenant_name,
|
||||
'trust_id': self.trust_id}
|
||||
|
||||
|
||||
class Token(Auth):
|
||||
"""A plugin for authenticating with an existing token.
|
||||
@@ -159,3 +168,10 @@ class Token(Auth):
|
||||
if headers is not None:
|
||||
headers['X-Auth-Token'] = self.token
|
||||
return {'token': {'id': self.token}}
|
||||
|
||||
def get_cache_id_elements(self):
|
||||
return {'token': self.token,
|
||||
'auth_url': self.auth_url,
|
||||
'tenant_id': self.tenant_id,
|
||||
'tenant_name': self.tenant_name,
|
||||
'trust_id': self.trust_id}
|
||||
|
||||
@@ -176,6 +176,29 @@ class Auth(BaseAuth):
|
||||
return access.AccessInfoV3(auth_token=resp.headers['X-Subject-Token'],
|
||||
body=resp_data)
|
||||
|
||||
def get_cache_id_elements(self):
|
||||
if not self.auth_methods:
|
||||
return None
|
||||
|
||||
params = {'auth_url': self.auth_url,
|
||||
'domain_id': self.domain_id,
|
||||
'domain_name': self.domain_name,
|
||||
'project_id': self.project_id,
|
||||
'project_name': self.project_name,
|
||||
'project_domain_id': self.project_domain_id,
|
||||
'project_domain_name': self.project_domain_name,
|
||||
'trust_id': self.trust_id}
|
||||
|
||||
for method in self.auth_methods:
|
||||
try:
|
||||
elements = method.get_cache_id_elements()
|
||||
except NotImplemented:
|
||||
return None
|
||||
|
||||
params.update(elements)
|
||||
|
||||
return params
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class AuthMethod(object):
|
||||
@@ -220,6 +243,22 @@ class AuthMethod(object):
|
||||
:rtype: tuple(string, dict)
|
||||
"""
|
||||
|
||||
def get_cache_id_elements(self):
|
||||
"""Get the elements for this auth method that make it unique.
|
||||
|
||||
These elements will be used as part of the
|
||||
:py:meth:`keystoneauth1.plugin.BaseIdentityPlugin.get_cache_id` to
|
||||
allow caching of the auth plugin.
|
||||
|
||||
Plugins should override this if they want to allow caching of their
|
||||
state.
|
||||
|
||||
To avoid collision or overrides the keys of the returned dictionary
|
||||
should be prefixed with the plugin identifier. For example the password
|
||||
plugin returns its username value as 'password_username'.
|
||||
"""
|
||||
raise NotImplemented()
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class AuthConstructor(Auth):
|
||||
|
||||
@@ -47,6 +47,10 @@ class PasswordMethod(base.AuthMethod):
|
||||
|
||||
return 'password', {'user': user}
|
||||
|
||||
def get_cache_id_elements(self):
|
||||
return dict(('password_%s' % p, getattr(self, p))
|
||||
for p in self._method_parameters)
|
||||
|
||||
|
||||
class Password(base.AuthConstructor):
|
||||
"""A plugin for authenticating with a username and password.
|
||||
|
||||
@@ -28,6 +28,9 @@ class TokenMethod(base.AuthMethod):
|
||||
headers['X-Auth-Token'] = self.token
|
||||
return 'token', {'id': self.token}
|
||||
|
||||
def get_cache_id_elements(self):
|
||||
return {'token_token': self.token}
|
||||
|
||||
|
||||
class Token(base.AuthConstructor):
|
||||
"""A plugin for authenticating with an existing Token.
|
||||
|
||||
@@ -197,3 +197,50 @@ class BaseAuthPlugin(object):
|
||||
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_cache_id(self):
|
||||
"""Fetch an identifier that uniquely identifies the auth options.
|
||||
|
||||
The returned identifier need not be decomposable or otherwise provide
|
||||
anyway to recreate the plugin. It should not contain sensitive data in
|
||||
plaintext.
|
||||
|
||||
This string MUST change if any of the parameters that are used to
|
||||
uniquely identity this plugin change.
|
||||
|
||||
If get_cache_id returns a str value suggesting that caching is
|
||||
supported then get_auth_cache and set_auth_cache must also be
|
||||
implemented.
|
||||
|
||||
:returns: A unique string for the set of options
|
||||
:rtype: str or None if this is unsupported or unavailable.
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_auth_state(self):
|
||||
"""Retrieve the current authentication state for the plugin.
|
||||
|
||||
Retrieve any internal state that represents the authenticated plugin.
|
||||
|
||||
This should not fetch any new data if it is not present.
|
||||
|
||||
:raises: keystoneclient.exceptions.NotImplementedError:
|
||||
if the plugin does not support this feature.
|
||||
|
||||
:returns: raw python data (which can be JSON serialized) that can be
|
||||
moved into another plugin (of the same type) to have the
|
||||
same authenticated state.
|
||||
:rtype: object or None if unauthenticated.
|
||||
"""
|
||||
return NotImplementedError()
|
||||
|
||||
def set_auth_state(self, data):
|
||||
"""Install existing authentication state for a plugin.
|
||||
|
||||
Take the output of get_auth_state and install that authentication state
|
||||
into the current authentication plugin.
|
||||
|
||||
:raises: keystoneclient.exceptions.NotImplementedError:
|
||||
if the plugin does not support this feature.
|
||||
"""
|
||||
return NotImplementedError()
|
||||
|
||||
@@ -235,6 +235,66 @@ class CommonIdentityTests(object):
|
||||
self.assertEqual(self.user_id, a.get_user_id(s))
|
||||
self.assertEqual(self.project_id, a.get_project_id(s))
|
||||
|
||||
def assertAccessInfoEqual(self, a, b):
|
||||
self.assertEqual(a.auth_token, b.auth_token)
|
||||
self.assertEqual(a._data, b._data)
|
||||
|
||||
def test_check_cache_id_match(self):
|
||||
a = self.create_auth_plugin()
|
||||
b = self.create_auth_plugin()
|
||||
|
||||
self.assertIsNot(a, b)
|
||||
self.assertIsNone(a.get_auth_state())
|
||||
self.assertIsNone(b.get_auth_state())
|
||||
|
||||
a_id = a.get_cache_id()
|
||||
b_id = b.get_cache_id()
|
||||
|
||||
self.assertIsNotNone(a_id)
|
||||
self.assertIsNotNone(b_id)
|
||||
|
||||
self.assertEqual(a_id, b_id)
|
||||
|
||||
def test_check_cache_id_no_match(self):
|
||||
a = self.create_auth_plugin(project_id='a')
|
||||
b = self.create_auth_plugin(project_id='b')
|
||||
|
||||
self.assertIsNot(a, b)
|
||||
self.assertIsNone(a.get_auth_state())
|
||||
self.assertIsNone(b.get_auth_state())
|
||||
|
||||
a_id = a.get_cache_id()
|
||||
b_id = b.get_cache_id()
|
||||
|
||||
self.assertIsNotNone(a_id)
|
||||
self.assertIsNotNone(b_id)
|
||||
|
||||
self.assertNotEqual(a_id, b_id)
|
||||
|
||||
def test_get_set_auth_state(self):
|
||||
a = self.create_auth_plugin()
|
||||
b = self.create_auth_plugin()
|
||||
|
||||
self.assertEqual(a.get_cache_id(), b.get_cache_id())
|
||||
|
||||
s = session.Session()
|
||||
|
||||
a_token = a.get_token(s)
|
||||
|
||||
self.assertEqual(1, self.requests_mock.call_count)
|
||||
|
||||
auth_state = a.get_auth_state()
|
||||
|
||||
self.assertIsNotNone(auth_state)
|
||||
|
||||
b.set_auth_state(auth_state)
|
||||
|
||||
b_token = b.get_token(s)
|
||||
self.assertEqual(1, self.requests_mock.call_count)
|
||||
|
||||
self.assertEqual(a_token, b_token)
|
||||
self.assertAccessInfoEqual(a.auth_ref, b.auth_ref)
|
||||
|
||||
|
||||
class V3(CommonIdentityTests, utils.TestCase):
|
||||
|
||||
@@ -281,6 +341,17 @@ class V2(CommonIdentityTests, utils.TestCase):
|
||||
kwargs.setdefault('auth_url', self.TEST_URL)
|
||||
kwargs.setdefault('username', self.TEST_USER)
|
||||
kwargs.setdefault('password', self.TEST_PASS)
|
||||
|
||||
try:
|
||||
kwargs.setdefault('tenant_id', kwargs.pop('project_id'))
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
kwargs.setdefault('tenant_name', kwargs.pop('project_name'))
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return identity.V2Password(**kwargs)
|
||||
|
||||
def get_auth_data(self, **kwargs):
|
||||
|
||||
@@ -11,9 +11,13 @@
|
||||
# under the License.
|
||||
|
||||
import copy
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from keystoneauth1 import _utils as ksa_utils
|
||||
from keystoneauth1 import access
|
||||
from keystoneauth1 import exceptions
|
||||
from keystoneauth1 import fixture
|
||||
from keystoneauth1.identity import v2
|
||||
from keystoneauth1 import session
|
||||
from keystoneauth1.tests.unit import utils
|
||||
@@ -299,3 +303,67 @@ class V2IdentityPlugin(utils.TestCase):
|
||||
def test_password_with_no_user_id_or_name(self):
|
||||
self.assertRaises(TypeError,
|
||||
v2.Password, self.TEST_URL, password=self.TEST_PASS)
|
||||
|
||||
def test_password_cache_id(self):
|
||||
self.stub_auth(json=self.TEST_RESPONSE_DICT)
|
||||
trust_id = uuid.uuid4().hex
|
||||
|
||||
a = v2.Password(self.TEST_URL,
|
||||
username=self.TEST_USER,
|
||||
password=self.TEST_PASS,
|
||||
trust_id=trust_id)
|
||||
|
||||
b = v2.Password(self.TEST_URL,
|
||||
username=self.TEST_USER,
|
||||
password=self.TEST_PASS,
|
||||
trust_id=trust_id)
|
||||
|
||||
a_id = a.get_cache_id()
|
||||
b_id = b.get_cache_id()
|
||||
|
||||
self.assertEqual(a_id, b_id)
|
||||
|
||||
c = v2.Password(self.TEST_URL,
|
||||
username=self.TEST_USER,
|
||||
password=self.TEST_PASS,
|
||||
tenant_id=trust_id) # same value different param
|
||||
|
||||
c_id = c.get_cache_id()
|
||||
|
||||
self.assertNotEqual(a_id, c_id)
|
||||
|
||||
self.assertIsNone(a.get_auth_state())
|
||||
self.assertIsNone(b.get_auth_state())
|
||||
self.assertIsNone(c.get_auth_state())
|
||||
|
||||
s = session.Session()
|
||||
self.assertEqual(self.TEST_TOKEN, a.get_token(s))
|
||||
self.assertTrue(self.requests_mock.called)
|
||||
|
||||
def test_password_change_auth_state(self):
|
||||
self.stub_auth(json=self.TEST_RESPONSE_DICT)
|
||||
|
||||
expired = ksa_utils.before_utcnow(days=2)
|
||||
token = fixture.V2Token(expires=expired)
|
||||
|
||||
auth_ref = access.create(body=token)
|
||||
|
||||
a = v2.Password(self.TEST_URL,
|
||||
username=self.TEST_USER,
|
||||
password=self.TEST_PASS,
|
||||
tenant_id=uuid.uuid4().hex)
|
||||
|
||||
initial_cache_id = a.get_cache_id()
|
||||
|
||||
state = a.get_auth_state()
|
||||
self.assertIsNone(state)
|
||||
|
||||
state = json.dumps({'auth_token': auth_ref.auth_token,
|
||||
'body': auth_ref._data})
|
||||
a.set_auth_state(state)
|
||||
|
||||
self.assertEqual(token.token_id, a.auth_ref.auth_token)
|
||||
|
||||
s = session.Session()
|
||||
self.assertEqual(self.TEST_TOKEN, a.get_token(s)) # updates expired
|
||||
self.assertEqual(initial_cache_id, a.get_cache_id())
|
||||
|
||||
@@ -11,8 +11,10 @@
|
||||
# under the License.
|
||||
|
||||
import copy
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from keystoneauth1 import _utils as ksa_utils
|
||||
from keystoneauth1 import access
|
||||
from keystoneauth1 import exceptions
|
||||
from keystoneauth1 import fixture
|
||||
@@ -560,3 +562,71 @@ class V3IdentityPlugin(utils.TestCase):
|
||||
s = session.Session()
|
||||
|
||||
self.assertRaises(exceptions.AuthorizationFailure, a.get_auth_ref, s)
|
||||
|
||||
def test_password_cache_id(self):
|
||||
self.stub_auth(json=self.TEST_RESPONSE_DICT)
|
||||
project_name = uuid.uuid4().hex
|
||||
|
||||
a = v3.Password(self.TEST_URL,
|
||||
username=self.TEST_USER,
|
||||
password=self.TEST_PASS,
|
||||
user_domain_id=self.TEST_DOMAIN_ID,
|
||||
project_domain_name=self.TEST_DOMAIN_NAME,
|
||||
project_name=project_name)
|
||||
|
||||
b = v3.Password(self.TEST_URL,
|
||||
username=self.TEST_USER,
|
||||
password=self.TEST_PASS,
|
||||
user_domain_id=self.TEST_DOMAIN_ID,
|
||||
project_domain_name=self.TEST_DOMAIN_NAME,
|
||||
project_name=project_name)
|
||||
|
||||
a_id = a.get_cache_id()
|
||||
b_id = b.get_cache_id()
|
||||
|
||||
self.assertEqual(a_id, b_id)
|
||||
|
||||
c = v3.Password(self.TEST_URL,
|
||||
username=self.TEST_USER,
|
||||
password=self.TEST_PASS,
|
||||
user_domain_id=self.TEST_DOMAIN_ID,
|
||||
project_domain_name=self.TEST_DOMAIN_NAME,
|
||||
project_id=project_name) # same value different param
|
||||
|
||||
c_id = c.get_cache_id()
|
||||
|
||||
self.assertNotEqual(a_id, c_id)
|
||||
|
||||
self.assertIsNone(a.get_auth_state())
|
||||
self.assertIsNone(b.get_auth_state())
|
||||
self.assertIsNone(c.get_auth_state())
|
||||
|
||||
s = session.Session()
|
||||
self.assertEqual(self.TEST_TOKEN, a.get_token(s))
|
||||
self.assertTrue(self.requests_mock.called)
|
||||
|
||||
def test_password_change_auth_state(self):
|
||||
self.stub_auth(json=self.TEST_RESPONSE_DICT)
|
||||
|
||||
expired = ksa_utils.before_utcnow(days=2)
|
||||
token = fixture.V3Token(expires=expired)
|
||||
token_id = uuid.uuid4().hex
|
||||
|
||||
state = json.dumps({'auth_token': token_id, 'body': token})
|
||||
|
||||
a = v3.Password(self.TEST_URL,
|
||||
username=self.TEST_USER,
|
||||
password=self.TEST_PASS,
|
||||
user_domain_id=self.TEST_DOMAIN_ID,
|
||||
project_id=uuid.uuid4().hex)
|
||||
|
||||
initial_cache_id = a.get_cache_id()
|
||||
|
||||
self.assertIsNone(a.get_auth_state())
|
||||
a.set_auth_state(state)
|
||||
|
||||
self.assertEqual(token_id, a.auth_ref.auth_token)
|
||||
|
||||
s = session.Session()
|
||||
self.assertEqual(self.TEST_TOKEN, a.get_token(s)) # updates expired
|
||||
self.assertEqual(initial_cache_id, a.get_cache_id())
|
||||
|
||||
Reference in New Issue
Block a user