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:
parent
94cbb61663
commit
5939541bc7
|
@ -15,6 +15,15 @@
|
|||
# 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):
|
||||
"""An object for encapsulating a raw authentication token from keystone
|
||||
and helper methods for extracting useful values from that token."""
|
||||
|
@ -22,6 +31,29 @@ class AccessInfo(dict):
|
|||
def __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
|
||||
def auth_token(self):
|
||||
""" Returns the token_id associated with the auth request, to be used
|
||||
|
|
|
@ -32,6 +32,16 @@ from keystoneclient import exceptions
|
|||
_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):
|
||||
|
||||
USER_AGENT = 'python-keystoneclient'
|
||||
|
@ -40,7 +50,8 @@ class HTTPClient(httplib2.Http):
|
|||
password=None, auth_url=None, region_name=None, timeout=None,
|
||||
endpoint=None, token=None, cacert=None, key=None,
|
||||
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)
|
||||
if cert:
|
||||
if key:
|
||||
|
@ -95,8 +106,141 @@ class HTTPClient(httplib2.Http):
|
|||
_logger.setLevel(logging.DEBUG)
|
||||
_logger.addHandler(ch)
|
||||
|
||||
def authenticate(self):
|
||||
""" Authenticate against the Identity API.
|
||||
# keyring setup
|
||||
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
|
||||
version-specific.
|
||||
|
@ -104,6 +248,9 @@ class HTTPClient(httplib2.Http):
|
|||
Expected to authenticate or validate an existing authentication
|
||||
reference already associated with the client. Invoking this call
|
||||
*always* makes a call to the Keystone.
|
||||
|
||||
:returns: ``raw token``
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
|
|
@ -25,6 +25,8 @@ import os
|
|||
import sys
|
||||
|
||||
import keystoneclient
|
||||
|
||||
from keystoneclient import access
|
||||
from keystoneclient import exceptions as exc
|
||||
from keystoneclient import utils
|
||||
from keystoneclient.v2_0 import shell as shell_v2_0
|
||||
|
@ -178,6 +180,37 @@ class OpenStackIdentityShell(object):
|
|||
'against any certificate authorities. This '
|
||||
'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):
|
||||
# deprecated command line options for essex compatibility. To be
|
||||
# removed in Grizzly release cycle.
|
||||
|
@ -375,7 +408,10 @@ class OpenStackIdentityShell(object):
|
|||
key=args.os_key,
|
||||
cert=args.os_cert,
|
||||
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:
|
||||
args.func(self.cs, args)
|
||||
|
|
|
@ -139,57 +139,41 @@ class Client(client.HTTPClient):
|
|||
"""Returns True if this client provides a service catalog."""
|
||||
return hasattr(self, 'service_catalog')
|
||||
|
||||
def authenticate(self, username=None, password=None, tenant_name=None,
|
||||
tenant_id=None, auth_url=None, token=None):
|
||||
def process_token(self):
|
||||
""" 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.
|
||||
|
||||
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.
|
||||
:returns: ``raw token`` if authentication was successful.
|
||||
:raises: AuthorizationFailure if unable to authenticate or validate
|
||||
the existing authorization token
|
||||
: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:
|
||||
raw_token = self._base_authN(auth_url,
|
||||
username=username,
|
||||
tenant_id=tenant_id,
|
||||
tenant_name=tenant_name,
|
||||
password=password,
|
||||
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
|
||||
return self._base_authN(auth_url,
|
||||
username=username,
|
||||
tenant_id=tenant_id,
|
||||
tenant_name=tenant_name,
|
||||
password=password,
|
||||
token=token)
|
||||
except (exceptions.AuthorizationFailure, exceptions.Unauthorized):
|
||||
_logger.debug("Authorization Failed.")
|
||||
raise
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import datetime
|
||||
|
||||
from keystoneclient import access
|
||||
from keystoneclient.openstack.common import timeutils
|
||||
from tests import utils
|
||||
from tests import client_fixtures
|
||||
|
||||
|
@ -28,6 +31,16 @@ class AccessInfoTest(utils.TestCase):
|
|||
|
||||
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):
|
||||
auth_ref = access.AccessInfo(PROJECT_SCOPED_TOKEN['access'])
|
||||
|
||||
|
|
|
@ -89,6 +89,24 @@ class ShellTest(utils.TestCase):
|
|||
'wilma', 'betty', '2.0')
|
||||
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):
|
||||
"""Test user-create args"""
|
||||
do_uc_mock = mock.MagicMock()
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
distribute>=0.6.24
|
||||
|
||||
coverage
|
||||
keyring
|
||||
mock
|
||||
mox
|
||||
nose
|
||||
|
|
Loading…
Reference in New Issue