From 02b637cdca6963e8dcab5170422347df99606f92 Mon Sep 17 00:00:00 2001 From: Charles Hsu Date: Wed, 18 Dec 2019 00:32:36 +0800 Subject: Support v3 application credentials auth. Use keystoneauth1 application credential plugin and session to fetch a token and endpoint catalog url. $ swift --os-auth-url http://172.16.1.2:5000/v3 --auth-version 3\ --os-application-credential-id THE_ID \ --os-application-credential-secret THE_SECRET \ --os-auth-type v3applicationcredential auth Change-Id: I9190e5e7e24b6a741970fa0d0ac792deccf73d25 Closes-Bug: 1843901 Closes-Bug: 1856635 --- swiftclient/client.py | 59 ++++++++++++++++++++++++++++++++++++------- swiftclient/service.py | 13 ++++++++++ swiftclient/shell.py | 54 +++++++++++++++++++++++++++++++++++---- test/unit/test_shell.py | 46 +++++++++++++++++++++++++++++++++ test/unit/test_swiftclient.py | 57 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 215 insertions(+), 14 deletions(-) diff --git a/swiftclient/client.py b/swiftclient/client.py index 449b6cd..ee85a14 100644 --- a/swiftclient/client.py +++ b/swiftclient/client.py @@ -70,6 +70,9 @@ except ImportError: pass try: from keystoneclient.v3 import client as ksclient_v3 + from keystoneauth1.identity import v3 + from keystoneauth1 import session + from keystoneauth1 import exceptions as ksauthexceptions except ImportError: pass @@ -615,6 +618,46 @@ Auth versions 2.0 and 3 require python-keystoneclient, install it or use Auth version 1.0 which requires ST_AUTH, ST_USER, and ST_KEY environment variables to be set or overridden with -A, -U, or -K.''') + filter_kwargs = {} + service_type = os_options.get('service_type') or 'object-store' + endpoint_type = os_options.get('endpoint_type') or 'publicURL' + if os_options.get('region_name'): + filter_kwargs['attr'] = 'region' + filter_kwargs['filter_value'] = os_options['region_name'] + + if os_options.get('auth_type') == 'v3applicationcredential': + try: + v3 + except NameError: + raise ClientException('Auth v3applicationcredential requires ' + 'python-keystoneclient>=2.0.0') + + try: + auth = v3.ApplicationCredential( + auth_url=auth_url, + application_credential_secret=os_options.get( + 'application_credential_secret'), + application_credential_id=os_options.get( + 'application_credential_id')) + sses = session.Session(auth=auth) + token = sses.get_token() + except ksauthexceptions.Unauthorized: + msg = 'Unauthorized. Check application credential id and secret.' + raise ClientException(msg) + except ksauthexceptions.AuthorizationFailure as err: + raise ClientException('Authorization Failure. %s' % err) + + try: + endpoint = sses.get_endpoint_data(service_type=service_type, + endpoint_type=endpoint_type, + **filter_kwargs) + + return endpoint.catalog_url, token + except ksauthexceptions.EndpointNotFound: + raise ClientException( + 'Endpoint for %s not found - ' + 'have you specified a region?' % service_type) + try: _ksclient = ksclient.Client( username=user, @@ -642,13 +685,8 @@ variables to be set or overridden with -A, -U, or -K.''') raise ClientException(msg) except ksexceptions.AuthorizationFailure as err: raise ClientException('Authorization Failure. %s' % err) - service_type = os_options.get('service_type') or 'object-store' - endpoint_type = os_options.get('endpoint_type') or 'publicURL' + try: - filter_kwargs = {} - if os_options.get('region_name'): - filter_kwargs['attr'] = 'region' - filter_kwargs['filter_value'] = os_options['region_name'] endpoint = _ksclient.service_catalog.url_for( service_type=service_type, endpoint_type=endpoint_type, @@ -717,9 +755,12 @@ def get_auth(auth_url, user, key, **kwargs): if kwargs.get('tenant_name'): os_options['tenant_name'] = kwargs['tenant_name'] - if not (os_options.get('tenant_name') or os_options.get('tenant_id') or - os_options.get('project_name') or - os_options.get('project_id')): + if os_options.get('auth_type') == 'v3applicationcredential': + pass + elif not (os_options.get('tenant_name') or + os_options.get('tenant_id') or + os_options.get('project_name') or + os_options.get('project_id')): if auth_version in AUTH_VERSIONS_V2: raise ClientException('No tenant specified') raise ClientException('No project name or project id specified.') diff --git a/swiftclient/service.py b/swiftclient/service.py index fb334fd..b89399f 100644 --- a/swiftclient/service.py +++ b/swiftclient/service.py @@ -110,6 +110,9 @@ def process_options(options): else: options['auth_version'] = '2.0' + if options.get('os_auth_type', None) == 'v3applicationcredential': + options['auth_version'] == '3' + # Use new-style args if old ones not present if not options['auth'] and options['os_auth_url']: options['auth'] = options['os_auth_url'] @@ -134,6 +137,11 @@ def process_options(options): 'auth_token': options['os_auth_token'], 'object_storage_url': options['os_storage_url'], 'region_name': options['os_region_name'], + 'auth_type': options['os_auth_type'], + 'application_credential_id': + options['os_application_credential_id'], + 'application_credential_secret': + options['os_application_credential_secret'], } @@ -162,6 +170,11 @@ def _build_default_global_options(): "os_project_domain_id": environ.get('OS_PROJECT_DOMAIN_ID'), "os_auth_url": environ.get('OS_AUTH_URL'), "os_auth_token": environ.get('OS_AUTH_TOKEN'), + "os_auth_type": environ.get('OS_AUTH_TYPE'), + "os_application_credential_id": + environ.get('OS_APPLICATION_CREDENTIAL_ID'), + "os_application_credential_secret": + environ.get('OS_APPLICATION_CREDENTIAL_SECRET'), "os_storage_url": environ.get('OS_STORAGE_URL'), "os_region_name": environ.get('OS_REGION_NAME'), "os_service_type": environ.get('OS_SERVICE_TYPE'), diff --git a/swiftclient/shell.py b/swiftclient/shell.py index 1b34c08..0fef755 100755 --- a/swiftclient/shell.py +++ b/swiftclient/shell.py @@ -1651,16 +1651,27 @@ def parse_args(parser, args, enforce_requires=True): return options, args if enforce_requires: - if options['auth_version'] == '3': + if options['os_auth_type'] == 'v3applicationcredential': + if not (options['os_application_credential_id'] and + options['os_application_credential_secret']): + exit('Auth version 3 (application credential) requires ' + 'OS_APPLICATION_CREDENTIAL_ID and ' + 'OS_APPLICATION_CREDENTIAL_SECRET to be set or ' + 'overridden with --os-application-credential-id and ' + '--os-application-credential-secret respectively.') + elif options['os_auth_type']: + exit('Only "v3applicationcredential" is supported for ' + '--os-auth-type') + elif options['auth_version'] == '3': if not options['auth']: - exit('Auth version 3 requires OS_AUTH_URL to be set or ' + + exit('Auth version 3 requires OS_AUTH_URL to be set or ' 'overridden with --os-auth-url') if not (options['user'] or options['os_user_id']): - exit('Auth version 3 requires either OS_USERNAME or ' + - 'OS_USER_ID to be set or overridden with ' + + exit('Auth version 3 requires either OS_USERNAME or ' + 'OS_USER_ID to be set or overridden with ' '--os-username or --os-user-id respectively.') if not options['key']: - exit('Auth version 3 requires OS_PASSWORD to be set or ' + + exit('Auth version 3 requires OS_PASSWORD to be set or ' 'overridden with --os-password') elif not (options['auth'] and options['user'] and options['key']): exit(''' @@ -1831,6 +1842,29 @@ def add_default_args(parser): 'env[OS_AUTH_URL].') os_grp.add_argument('--os_auth_url', help=argparse.SUPPRESS) + os_grp.add_argument('--os-auth-type', + metavar='', + default=environ.get('OS_AUTH_TYPE'), + help='OpenStack auth type for v3. Defaults to ' + 'env[OS_AUTH_TYPE].') + os_grp.add_argument('--os_auth_type', + help=argparse.SUPPRESS) + os_grp.add_argument('--os-application-credential-id', + metavar='', + default=environ.get('OS_APPLICATION_CREDENTIAL_ID'), + help='OpenStack appplication credential id. ' + 'Defaults to env[OS_APPLICATION_CREDENTIAL_ID].') + os_grp.add_argument('--os_application_credential_id', + help=argparse.SUPPRESS) + os_grp.add_argument('--os-application-credential-secret', + metavar='', + default=environ.get( + 'OS_APPLICATION_CREDENTIAL_SECRET'), + help='OpenStack appplication credential secret. ' + 'Defaults to ' + 'env[OS_APPLICATION_CREDENTIAL_SECRET].') + os_grp.add_argument('--os_application_credential_secret', + help=argparse.SUPPRESS) os_grp.add_argument('--os-auth-token', metavar='', default=environ.get('OS_AUTH_TOKEN'), @@ -1915,6 +1949,11 @@ def main(arguments=None): [--os-project-domain-name ] [--os-auth-url ] [--os-auth-token ] + [--os-auth-type ] + [--os-application-credential-id + ] + [--os-application-credential-secret + ] [--os-storage-url ] [--os-region-name ] [--os-service-type ] @@ -1967,6 +2006,11 @@ Examples: --os-user-id abcdef0123456789abcdef0123456789 \\ --os-password password list + %(prog)s --os-auth-url https://api.example.com/v3 --auth-version 3\\ + --os-application-credential-id d78683c92f0e4f9b9b02a2e208039412 \\ + --os-application-credential-secret APPLICTION_CREDENTIAL_SECRET \\ + --os-auth-type v3applicationcredential list + %(prog)s --os-auth-token 6ee5eb33efad4e45ab46806eac010566 \\ --os-storage-url https://10.1.5.2:8080/v1/AUTH_ced809b6a4baea7aeab61a \\ list diff --git a/test/unit/test_shell.py b/test/unit/test_shell.py index a63d16b..3c08218 100644 --- a/test/unit/test_shell.py +++ b/test/unit/test_shell.py @@ -2395,6 +2395,8 @@ class TestParsing(TestBase): 'object_storage_url', 'project_domain_id', 'user_id', 'user_domain_id', 'tenant_id', 'service_type', 'project_id', 'auth_token', + 'auth_type', 'application_credential_id', + 'application_credential_secret', 'project_domain_name'] for key in expected_os_opts_keys: self.assertIn(key, actual_os_opts_dict) @@ -2686,6 +2688,50 @@ class TestParsing(TestBase): swiftclient.shell.main(args) self.assertIn('Auth version 3 requires OS_AUTH_URL', str(cm.exception)) + def test_command_args_v3applicationcredential(self): + result = [None, None] + fake_command = self._make_fake_command(result) + opts = {"auth_version": "3"} + os_opts = { + "auth_type": "v3applicationcredential", + "application_credential_id": "proejct_id", + "application_credential_secret": "secret", + "auth_url": "http://example.com:5000/v3"} + + args = _make_args("stat", opts, os_opts) + with mock.patch('swiftclient.shell.st_stat', fake_command): + swiftclient.shell.main(args) + self.assertEqual(['stat'], result[1]) + with mock.patch('swiftclient.shell.st_stat', fake_command): + args = args + ["container_name"] + swiftclient.shell.main(args) + self.assertEqual(["stat", "container_name"], result[1]) + + def test_insufficient_args_v3applicationcredential(self): + opts = {"auth_version": "3"} + os_opts = { + "auth_type": "v3applicationcredential", + "application_credential_secret": "secret", + "auth_url": "http://example.com:5000/v3"} + + args = _make_args("stat", opts, os_opts) + with self.assertRaises(SystemExit) as cm: + swiftclient.shell.main(args) + self.assertIn('Auth version 3 (application credential) requires', + str(cm.exception)) + + os_opts = { + "auth_type": "v3password", + "application_credential_id": "proejct_id", + "application_credential_secret": "secret", + "auth_url": "http://example.com:5000/v3"} + + args = _make_args("stat", opts, os_opts) + with self.assertRaises(SystemExit) as cm: + swiftclient.shell.main(args) + self.assertIn('Only "v3applicationcredential" is supported for', + str(cm.exception)) + def test_password_prompt(self): def do_test(opts, os_opts, auth_version): args = _make_args("stat", opts, os_opts) diff --git a/test/unit/test_swiftclient.py b/test/unit/test_swiftclient.py index 2d45deb..7354bdc 100644 --- a/test/unit/test_swiftclient.py +++ b/test/unit/test_swiftclient.py @@ -562,6 +562,63 @@ class TestGetAuth(MockHttpTest): self.assertTrue(url.startswith("http")) self.assertTrue(token) + def test_auth_v3applicationcredential(self): + from keystoneauth1 import exceptions as ksauthexceptions + + os_options = { + "auth_type": "v3applicationcredential", + "application_credential_id": "proejct_id", + "application_credential_secret": "secret"} + + class FakeEndpointData(object): + catalog_url = 'http://swift.cluster/v1/KEY_project_id' + + class FakeKeystoneuth1v3Session(object): + + def __init__(self, auth): + self.auth = auth + self.token = 'token' + + def get_token(self): + if self.auth.auth_url == 'http://keystone:5000/v3': + return self.token + elif self.auth.auth_url == 'http://keystone:9000/v3': + raise ksauthexceptions.AuthorizationFailure + else: + raise ksauthexceptions.Unauthorized + + def get_endpoint_data(self, service_type, endpoint_type, **kwargs): + return FakeEndpointData() + + mock_sess = FakeKeystoneuth1v3Session + with mock.patch('keystoneauth1.session.Session', mock_sess): + url, token = c.get_auth('http://keystone:5000', '', '', + os_options=os_options, + auth_version="3") + + self.assertTrue(url.startswith("http")) + self.assertEqual(url, 'http://swift.cluster/v1/KEY_project_id') + self.assertEqual(token, 'token') + + with mock.patch('keystoneauth1.session.Session', mock_sess): + with self.assertRaises(c.ClientException) as exc_mgr: + url, token = c.get_auth('http://keystone:9000', '', '', + os_options=os_options, + auth_version="3") + + body = 'Unauthorized. Check application credential id and secret.' + body = 'Authorization Failure. Cannot authorize API client.' + self.assertEqual(exc_mgr.exception.__str__()[-89:], body) + + with mock.patch('keystoneauth1.session.Session', mock_sess): + with self.assertRaises(c.ClientException) as exc_mgr: + url, token = c.get_auth('http://keystone:5000', '', '', + os_options=os_options, + auth_version="2") + + body = 'Unauthorized. Check application credential id and secret.' + self.assertEqual(exc_mgr.exception.__str__()[-89:], body) + def test_get_keystone_client_2_0(self): # check the correct auth version is passed to get_auth_keystone os_options = {'tenant_name': 'asdf'} -- cgit v1.2.1