bug-1040361: use keyring to store tokens
User can optionally turn off keyring by specifying the --no-cache option. It can also be disabled with environment variable OS-NO-CACHE. Change-Id: I8935260bf7fd6befa14798da9b4d02c81e65c417
This commit is contained in:
@@ -15,6 +15,15 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from keystoneclient.openstack.common import timeutils
|
||||||
|
|
||||||
|
|
||||||
|
# gap, in seconds, to determine whether the given token is about to expire
|
||||||
|
STALE_TOKEN_DURATION = '30'
|
||||||
|
|
||||||
|
|
||||||
class AccessInfo(dict):
|
class AccessInfo(dict):
|
||||||
"""An object for encapsulating a raw authentication token from keystone
|
"""An object for encapsulating a raw authentication token from keystone
|
||||||
and helper methods for extracting useful values from that token."""
|
and helper methods for extracting useful values from that token."""
|
||||||
@@ -22,6 +31,29 @@ class AccessInfo(dict):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
dict.__init__(self, *args, **kwargs)
|
dict.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
|
def will_expire_soon(self, stale_duration=None):
|
||||||
|
""" Determines if expiration is about to occur.
|
||||||
|
|
||||||
|
:return: boolean : true if expiration is within the given duration
|
||||||
|
|
||||||
|
"""
|
||||||
|
stale_duration = stale_duration or TALE_TOKEN_DURATION
|
||||||
|
norm_expires = timeutils.normalize_time(self.expires)
|
||||||
|
# (gyee) should we move auth_token.will_expire_soon() to timeutils
|
||||||
|
# instead of duplicating code here?
|
||||||
|
soon = (timeutils.utcnow() + datetime.timedelta(
|
||||||
|
seconds=stale_duration))
|
||||||
|
return norm_expires < soon
|
||||||
|
|
||||||
|
@property
|
||||||
|
def expires(self):
|
||||||
|
""" Returns the token expiration (as datetime object)
|
||||||
|
|
||||||
|
:returns: datetime
|
||||||
|
|
||||||
|
"""
|
||||||
|
return timeutils.parse_isotime(self['token']['expires'])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def auth_token(self):
|
def auth_token(self):
|
||||||
""" Returns the token_id associated with the auth request, to be used
|
""" Returns the token_id associated with the auth request, to be used
|
||||||
|
|||||||
@@ -32,6 +32,16 @@ from keystoneclient import exceptions
|
|||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# keyring init
|
||||||
|
keyring_available = True
|
||||||
|
try:
|
||||||
|
import keyring
|
||||||
|
import pickle
|
||||||
|
except ImportError:
|
||||||
|
_logger.warning('Failed to load keyring modules.')
|
||||||
|
keyring_available = False
|
||||||
|
|
||||||
|
|
||||||
class HTTPClient(httplib2.Http):
|
class HTTPClient(httplib2.Http):
|
||||||
|
|
||||||
USER_AGENT = 'python-keystoneclient'
|
USER_AGENT = 'python-keystoneclient'
|
||||||
@@ -40,7 +50,8 @@ class HTTPClient(httplib2.Http):
|
|||||||
password=None, auth_url=None, region_name=None, timeout=None,
|
password=None, auth_url=None, region_name=None, timeout=None,
|
||||||
endpoint=None, token=None, cacert=None, key=None,
|
endpoint=None, token=None, cacert=None, key=None,
|
||||||
cert=None, insecure=False, original_ip=None, debug=False,
|
cert=None, insecure=False, original_ip=None, debug=False,
|
||||||
auth_ref=None):
|
auth_ref=None, use_keyring=True, force_new_token=False,
|
||||||
|
stale_duration=None):
|
||||||
super(HTTPClient, self).__init__(timeout=timeout, ca_certs=cacert)
|
super(HTTPClient, self).__init__(timeout=timeout, ca_certs=cacert)
|
||||||
if cert:
|
if cert:
|
||||||
if key:
|
if key:
|
||||||
@@ -95,8 +106,141 @@ class HTTPClient(httplib2.Http):
|
|||||||
_logger.setLevel(logging.DEBUG)
|
_logger.setLevel(logging.DEBUG)
|
||||||
_logger.addHandler(ch)
|
_logger.addHandler(ch)
|
||||||
|
|
||||||
def authenticate(self):
|
# keyring setup
|
||||||
""" Authenticate against the Identity API.
|
self.use_keyring = use_keyring and keyring_available
|
||||||
|
self.force_new_token = force_new_token
|
||||||
|
self.stale_duration = stale_duration or access.STALE_TOKEN_DURATION
|
||||||
|
self.stale_duration = int(self.stale_duration)
|
||||||
|
|
||||||
|
def authenticate(self, username=None, password=None, tenant_name=None,
|
||||||
|
tenant_id=None, auth_url=None, token=None):
|
||||||
|
""" Authenticate user.
|
||||||
|
|
||||||
|
Uses the data provided at instantiation to authenticate against
|
||||||
|
the Keystone server. This may use either a username and password
|
||||||
|
or token for authentication. If a tenant name or id was provided
|
||||||
|
then the resulting authenticated client will be scoped to that
|
||||||
|
tenant and contain a service catalog of available endpoints.
|
||||||
|
|
||||||
|
With the v2.0 API, if a tenant name or ID is not provided, the
|
||||||
|
authenication token returned will be 'unscoped' and limited in
|
||||||
|
capabilities until a fully-scoped token is acquired.
|
||||||
|
|
||||||
|
If successful, sets the self.auth_ref and self.auth_token with
|
||||||
|
the returned token. If not already set, will also set
|
||||||
|
self.management_url from the details provided in the token.
|
||||||
|
|
||||||
|
:returns: ``True`` if authentication was successful.
|
||||||
|
:raises: AuthorizationFailure if unable to authenticate or validate
|
||||||
|
the existing authorization token
|
||||||
|
:raises: ValueError if insufficient parameters are used.
|
||||||
|
|
||||||
|
If keyring is used, token is retrieved from keyring instead.
|
||||||
|
Authentication will only be necessary if any of the following
|
||||||
|
conditions are met:
|
||||||
|
|
||||||
|
* keyring is not used
|
||||||
|
* if token is not found in keyring
|
||||||
|
* if token retrieved from keyring is expired or about to
|
||||||
|
expired (as determined by stale_duration)
|
||||||
|
* if force_new_token is true
|
||||||
|
|
||||||
|
"""
|
||||||
|
auth_url = auth_url or self.auth_url
|
||||||
|
username = username or self.username
|
||||||
|
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
|
||||||
|
|
||||||
|
(keyring_key, auth_ref) = self.get_auth_ref_from_keyring(auth_url,
|
||||||
|
username,
|
||||||
|
tenant_name,
|
||||||
|
tenant_id,
|
||||||
|
token)
|
||||||
|
new_token_needed = False
|
||||||
|
if auth_ref is None or self.force_new_token:
|
||||||
|
new_token_needed = True
|
||||||
|
raw_token = self.get_raw_token_from_identity_service(auth_url,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
tenant_name,
|
||||||
|
tenant_id,
|
||||||
|
token)
|
||||||
|
self.auth_ref = access.AccessInfo(**raw_token)
|
||||||
|
else:
|
||||||
|
self.auth_ref = auth_ref
|
||||||
|
self.process_token()
|
||||||
|
if new_token_needed:
|
||||||
|
self.store_auth_ref_into_keyring(keyring_key)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _build_keyring_key(self, auth_url, username, tenant_name,
|
||||||
|
tenant_id, token):
|
||||||
|
""" Create a unique key for keyring.
|
||||||
|
|
||||||
|
Used to store and retrieve auth_ref from keyring.
|
||||||
|
|
||||||
|
"""
|
||||||
|
keys = [auth_url, username, tenant_name, tenant_id, token]
|
||||||
|
for index, key in enumerate(keys):
|
||||||
|
if key is None:
|
||||||
|
keys[index] = '?'
|
||||||
|
keyring_key = '/'.join(keys)
|
||||||
|
return keyring_key
|
||||||
|
|
||||||
|
def get_auth_ref_from_keyring(self, auth_url, username, tenant_name,
|
||||||
|
tenant_id, token):
|
||||||
|
""" Retrieve auth_ref from keyring.
|
||||||
|
|
||||||
|
If auth_ref is found in keyring, (keyring_key, auth_ref) is returned.
|
||||||
|
Otherwise, (keyring_key, None) is returned.
|
||||||
|
|
||||||
|
:returns: (keyring_key, auth_ref) or (keyring_key, None)
|
||||||
|
|
||||||
|
"""
|
||||||
|
keyring_key = None
|
||||||
|
auth_ref = None
|
||||||
|
if self.use_keyring:
|
||||||
|
keyring_key = self._build_keyring_key(auth_url, username,
|
||||||
|
tenant_name, tenant_id,
|
||||||
|
token)
|
||||||
|
try:
|
||||||
|
auth_ref = keyring.get_password("keystoneclient_auth",
|
||||||
|
keyring_key)
|
||||||
|
if auth_ref:
|
||||||
|
auth_ref = pickle.loads(auth_ref)
|
||||||
|
if auth_ref.will_expire_soon(self.stale_duration):
|
||||||
|
# token has expired, don't use it
|
||||||
|
auth_ref = None
|
||||||
|
except Exception as e:
|
||||||
|
auth_ref = None
|
||||||
|
_logger.warning('Unable to retrieve token from keyring %s' % (
|
||||||
|
e))
|
||||||
|
return (keyring_key, auth_ref)
|
||||||
|
|
||||||
|
def store_auth_ref_into_keyring(self, keyring_key):
|
||||||
|
""" Store auth_ref into keyring.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if self.use_keyring:
|
||||||
|
try:
|
||||||
|
keyring.set_password("keystoneclient_auth",
|
||||||
|
keyring_key,
|
||||||
|
pickle.dumps(self.auth_ref))
|
||||||
|
except Exception as e:
|
||||||
|
_logger.warning("Failed to store token into keyring %s" % (e))
|
||||||
|
|
||||||
|
def process_token(self):
|
||||||
|
""" Extract and process information from the new auth_ref.
|
||||||
|
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def get_raw_token_from_identity_service(self, auth_url, username=None,
|
||||||
|
password=None, tenant_name=None,
|
||||||
|
tenant_id=None, token=None):
|
||||||
|
""" Authenticate against the Identity API and get a token.
|
||||||
|
|
||||||
Not implemented here because auth protocols should be API
|
Not implemented here because auth protocols should be API
|
||||||
version-specific.
|
version-specific.
|
||||||
@@ -104,6 +248,9 @@ class HTTPClient(httplib2.Http):
|
|||||||
Expected to authenticate or validate an existing authentication
|
Expected to authenticate or validate an existing authentication
|
||||||
reference already associated with the client. Invoking this call
|
reference already associated with the client. Invoking this call
|
||||||
*always* makes a call to the Keystone.
|
*always* makes a call to the Keystone.
|
||||||
|
|
||||||
|
:returns: ``raw token``
|
||||||
|
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
import keystoneclient
|
import keystoneclient
|
||||||
|
|
||||||
|
from keystoneclient import access
|
||||||
from keystoneclient import exceptions as exc
|
from keystoneclient import exceptions as exc
|
||||||
from keystoneclient import utils
|
from keystoneclient import utils
|
||||||
from keystoneclient.v2_0 import shell as shell_v2_0
|
from keystoneclient.v2_0 import shell as shell_v2_0
|
||||||
@@ -178,6 +180,37 @@ class OpenStackIdentityShell(object):
|
|||||||
'against any certificate authorities. This '
|
'against any certificate authorities. This '
|
||||||
'option should be used with caution.')
|
'option should be used with caution.')
|
||||||
|
|
||||||
|
parser.add_argument('--no-cache',
|
||||||
|
default=env('OS_NO_CACHE',
|
||||||
|
default=False),
|
||||||
|
action='store_true',
|
||||||
|
help='Don\'t use the auth token cache. '
|
||||||
|
'Default to env[OS_NO_CACHE]')
|
||||||
|
parser.add_argument('--no_cache',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
parser.add_argument('--force-new-token',
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
dest='force_new_token',
|
||||||
|
help="If keyring is available and in used, "
|
||||||
|
"token will always be stored and fetched "
|
||||||
|
"from the keyring, until the token has "
|
||||||
|
"expired. Use this option to request a "
|
||||||
|
"new token and replace the existing one "
|
||||||
|
"in keyring.")
|
||||||
|
|
||||||
|
parser.add_argument('--stale-duration',
|
||||||
|
metavar='<seconds>',
|
||||||
|
default=access.STALE_TOKEN_DURATION,
|
||||||
|
dest='stale_duration',
|
||||||
|
help="Stale duration (in seconds) used to "
|
||||||
|
"determine whether a token has expired "
|
||||||
|
"when retrieving it from keyring. This "
|
||||||
|
"is useful in mitigating process or "
|
||||||
|
"network delays. Default is %s seconds." % (
|
||||||
|
access.STALE_TOKEN_DURATION))
|
||||||
|
|
||||||
#FIXME(heckj):
|
#FIXME(heckj):
|
||||||
# deprecated command line options for essex compatibility. To be
|
# deprecated command line options for essex compatibility. To be
|
||||||
# removed in Grizzly release cycle.
|
# removed in Grizzly release cycle.
|
||||||
@@ -375,7 +408,10 @@ class OpenStackIdentityShell(object):
|
|||||||
key=args.os_key,
|
key=args.os_key,
|
||||||
cert=args.os_cert,
|
cert=args.os_cert,
|
||||||
insecure=args.insecure,
|
insecure=args.insecure,
|
||||||
debug=args.debug)
|
debug=args.debug,
|
||||||
|
use_keyring=(not args.no_cache),
|
||||||
|
force_new_token=args.force_new_token,
|
||||||
|
stale_duration=args.stale_duration)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
args.func(self.cs, args)
|
args.func(self.cs, args)
|
||||||
|
|||||||
@@ -139,57 +139,41 @@ class Client(client.HTTPClient):
|
|||||||
"""Returns True if this client provides a service catalog."""
|
"""Returns True if this client provides a service catalog."""
|
||||||
return hasattr(self, 'service_catalog')
|
return hasattr(self, 'service_catalog')
|
||||||
|
|
||||||
def authenticate(self, username=None, password=None, tenant_name=None,
|
def process_token(self):
|
||||||
tenant_id=None, auth_url=None, token=None):
|
""" Extract and process information from the new auth_ref.
|
||||||
|
|
||||||
|
And set the relevant authentication information.
|
||||||
|
"""
|
||||||
|
# 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:
|
||||||
|
self.management_url = self.auth_ref.management_url[0]
|
||||||
|
self.tenant_name = self.auth_ref.tenant_name
|
||||||
|
self.tenant_id = self.auth_ref.tenant_id
|
||||||
|
self.user_id = self.auth_ref.user_id
|
||||||
|
self._extract_service_catalog(self.auth_url, self.auth_ref)
|
||||||
|
|
||||||
|
def get_raw_token_from_identity_service(self, auth_url, username=None,
|
||||||
|
password=None, tenant_name=None,
|
||||||
|
tenant_id=None, token=None):
|
||||||
""" Authenticate against the Keystone API.
|
""" Authenticate against the Keystone API.
|
||||||
|
|
||||||
Uses the data provided at instantiation to authenticate against
|
:returns: ``raw token`` if authentication was successful.
|
||||||
the Keystone server. This may use either a username and password
|
|
||||||
or token for authentication. If a tenant name or id was provided
|
|
||||||
then the resulting authenticated client will be scoped to that
|
|
||||||
tenant and contain a service catalog of available endpoints.
|
|
||||||
|
|
||||||
With the v2.0 API, if a tenant name or ID is not provided, the
|
|
||||||
authenication token returned will be 'unscoped' and limited in
|
|
||||||
capabilities until a fully-scoped token is acquired.
|
|
||||||
|
|
||||||
If successful, sets the self.auth_ref and self.auth_token with
|
|
||||||
the returned token. If not already set, will also set
|
|
||||||
self.management_url from the details provided in the token.
|
|
||||||
|
|
||||||
:returns: ``True`` if authentication was successful.
|
|
||||||
:raises: AuthorizationFailure if unable to authenticate or validate
|
:raises: AuthorizationFailure if unable to authenticate or validate
|
||||||
the existing authorization token
|
the existing authorization token
|
||||||
:raises: ValueError if insufficient parameters are used.
|
:raises: ValueError if insufficient parameters are used.
|
||||||
"""
|
|
||||||
auth_url = auth_url or self.auth_url
|
|
||||||
username = username or self.username
|
|
||||||
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
|
|
||||||
|
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
raw_token = self._base_authN(auth_url,
|
return self._base_authN(auth_url,
|
||||||
username=username,
|
username=username,
|
||||||
tenant_id=tenant_id,
|
tenant_id=tenant_id,
|
||||||
tenant_name=tenant_name,
|
tenant_name=tenant_name,
|
||||||
password=password,
|
password=password,
|
||||||
token=token)
|
token=token)
|
||||||
self.auth_ref = access.AccessInfo(**raw_token)
|
|
||||||
# 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]
|
|
||||||
self.tenant_name = self.auth_ref.tenant_name
|
|
||||||
self.tenant_id = self.auth_ref.tenant_id
|
|
||||||
self.user_id = self.auth_ref.user_id
|
|
||||||
self._extract_service_catalog(self.auth_url, self.auth_ref)
|
|
||||||
return True
|
|
||||||
except (exceptions.AuthorizationFailure, exceptions.Unauthorized):
|
except (exceptions.AuthorizationFailure, exceptions.Unauthorized):
|
||||||
_logger.debug("Authorization Failed.")
|
_logger.debug("Authorization Failed.")
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
from keystoneclient import access
|
from keystoneclient import access
|
||||||
|
from keystoneclient.openstack.common import timeutils
|
||||||
from tests import utils
|
from tests import utils
|
||||||
from tests import client_fixtures
|
from tests import client_fixtures
|
||||||
|
|
||||||
@@ -28,6 +31,16 @@ class AccessInfoTest(utils.TestCase):
|
|||||||
|
|
||||||
self.assertFalse(auth_ref.scoped)
|
self.assertFalse(auth_ref.scoped)
|
||||||
|
|
||||||
|
self.assertEquals(auth_ref.expires, timeutils.parse_isotime(
|
||||||
|
UNSCOPED_TOKEN['access']['token']['expires']))
|
||||||
|
|
||||||
|
def test_will_expire_soon(self):
|
||||||
|
expires = timeutils.utcnow() + datetime.timedelta(minutes=5)
|
||||||
|
UNSCOPED_TOKEN['access']['token']['expires'] = expires.isoformat()
|
||||||
|
auth_ref = access.AccessInfo(UNSCOPED_TOKEN['access'])
|
||||||
|
self.assertFalse(auth_ref.will_expire_soon(stale_duration=120))
|
||||||
|
self.assertTrue(auth_ref.will_expire_soon(stale_duration=300))
|
||||||
|
|
||||||
def test_building_scoped_accessinfo(self):
|
def test_building_scoped_accessinfo(self):
|
||||||
auth_ref = access.AccessInfo(PROJECT_SCOPED_TOKEN['access'])
|
auth_ref = access.AccessInfo(PROJECT_SCOPED_TOKEN['access'])
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,24 @@ class ShellTest(utils.TestCase):
|
|||||||
'wilma', 'betty', '2.0')
|
'wilma', 'betty', '2.0')
|
||||||
self.assertTrue(all([x == y for x, y in zip(actual, expect)]))
|
self.assertTrue(all([x == y for x, y in zip(actual, expect)]))
|
||||||
|
|
||||||
|
# Test keyring options
|
||||||
|
shell('--os-auth-url http://1.1.1.1:5000/ --os-password xyzpdq '
|
||||||
|
'--os-tenant-id 4321 --os-tenant-name wilma '
|
||||||
|
'--os-username betty '
|
||||||
|
'--os-identity-api-version 2.0 '
|
||||||
|
'--no-cache '
|
||||||
|
'--stale-duration 500 '
|
||||||
|
'--force-new-token user-list')
|
||||||
|
assert do_tenant_mock.called
|
||||||
|
((a, b), c) = do_tenant_mock.call_args
|
||||||
|
actual = (b.os_auth_url, b.os_password, b.os_tenant_id,
|
||||||
|
b.os_tenant_name, b.os_username,
|
||||||
|
b.os_identity_api_version, b.no_cache,
|
||||||
|
b.stale_duration, b.force_new_token)
|
||||||
|
expect = ('http://1.1.1.1:5000/', 'xyzpdq', '4321',
|
||||||
|
'wilma', 'betty', '2.0', True, '500', True)
|
||||||
|
self.assertTrue(all([x == y for x, y in zip(actual, expect)]))
|
||||||
|
|
||||||
def test_shell_user_create_args(self):
|
def test_shell_user_create_args(self):
|
||||||
"""Test user-create args"""
|
"""Test user-create args"""
|
||||||
do_uc_mock = mock.MagicMock()
|
do_uc_mock = mock.MagicMock()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
distribute>=0.6.24
|
distribute>=0.6.24
|
||||||
|
|
||||||
coverage
|
coverage
|
||||||
|
keyring
|
||||||
mock
|
mock
|
||||||
mox
|
mox
|
||||||
nose
|
nose
|
||||||
|
|||||||
Reference in New Issue
Block a user