Repository: libcloud Updated Branches: refs/heads/trunk 465e17bb3 -> e9ebac613
Add OpenStackIdentity_3_0_Connection_OIDC class to manage auth with OpenID tokens Closes #789 Signed-off-by: Tomaz Muraus <to...@tomaz.me> Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/8f97bb40 Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/8f97bb40 Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/8f97bb40 Branch: refs/heads/trunk Commit: 8f97bb4022662cd4c7763416b7e7ea1c8839c597 Parents: 465e17b Author: micafer <micaf...@upv.es> Authored: Tue May 17 17:27:01 2016 +0200 Committer: Tomaz Muraus <to...@tomaz.me> Committed: Thu May 26 20:52:44 2016 +0200 ---------------------------------------------------------------------- libcloud/common/openstack_identity.py | 151 ++++++++++++++++++++++++++++- 1 file changed, 150 insertions(+), 1 deletion(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/libcloud/blob/8f97bb40/libcloud/common/openstack_identity.py ---------------------------------------------------------------------- diff --git a/libcloud/common/openstack_identity.py b/libcloud/common/openstack_identity.py index ee07843..25c6df6 100644 --- a/libcloud/common/openstack_identity.py +++ b/libcloud/common/openstack_identity.py @@ -42,7 +42,8 @@ AUTH_VERSIONS_WITH_EXPIRES = [ '2.0_apikey', '2.0_password', '3.0', - '3.x_password' + '3.x_password', + '3.x_oidc' ] # How many seconds to subtract from the auth token expiration time before @@ -69,6 +70,7 @@ __all__ = [ 'OpenStackIdentity_1_1_Connection', 'OpenStackIdentity_2_0_Connection', 'OpenStackIdentity_3_0_Connection', + 'OpenStackIdentity_3_0_Connection_OIDC', 'get_class_for_auth_version' ] @@ -1377,6 +1379,151 @@ class OpenStackIdentity_3_0_Connection(OpenStackIdentityConnection): return role +class OpenStackIdentity_3_0_Connection_OIDC(OpenStackIdentity_3_0_Connection): + """ + Connection class for Keystone API v3.x. using OpenID Connect tokens + """ + + responseCls = OpenStackAuthResponse + name = 'OpenStack Identity API v3.x with OIDC support' + auth_version = '3.0' + + def get_unscoped_token_from_oidc_token(self): + """ + Get unscoped token from OIDC token + The OIDC token must be set in the self.key attribute. + The identity provider name required to get the full path + must be set in the self.tenant_name attribute. + """ + path = ('/v3/OS-FEDERATION/identity_providers/%s/protocols/oidc/auth' % + self.tenant_name) + response = self.request(path, + headers={'Content-Type': 'application/json', + 'Authorization': 'Bearer %s' % + self.key}, + method='GET') + + if response.status == httplib.UNAUTHORIZED: + # Invalid credentials + raise InvalidCredsError() + elif response.status in [httplib.OK, httplib.CREATED]: + if 'x-subject-token' in response.headers: + return response.headers['x-subject-token'] + else: + raise MalformedResponseError('No x-subject-token returned', + driver=self.driver) + else: + raise MalformedResponseError('Malformed response', + driver=self.driver) + + def get_project_id(self, token): + """ + Get the first project ID accessible with the specified token + """ + path = '/v3/OS-FEDERATION/projects' + response = self.request(path, + headers={'Content-Type': 'application/json', + 'X-Auth-Token': token}, + method='GET') + + if response.status == httplib.UNAUTHORIZED: + # Invalid credentials + raise InvalidCredsError() + elif response.status in [httplib.OK, httplib.CREATED]: + try: + body = json.loads(response.body) + return body["projects"][0]["id"] + except Exception: + e = sys.exc_info()[1] + raise MalformedResponseError('Failed to parse JSON', e) + else: + raise MalformedResponseError('Malformed response', + driver=self.driver) + + def authenticate(self, force=False): + """ + Perform authentication. + """ + if not self._is_authentication_needed(force=force): + return self + + subject_token = self.get_unscoped_token_from_oidc_token() + project_id = self.get_project_id(subject_token) + + data = { + 'auth': { + 'identity': { + 'methods': ['token'], + 'token': { + 'id': subject_token + } + } + } + } + + if self.token_scope == OpenStackIdentityTokenScope.PROJECT: + # Scope token to project (tenant) + data['auth']['scope'] = { + 'project': { + 'id': project_id + } + } + elif self.token_scope == OpenStackIdentityTokenScope.DOMAIN: + # Scope token to domain + data['auth']['scope'] = { + 'domain': { + 'name': self.domain_name + } + } + elif self.token_scope == OpenStackIdentityTokenScope.UNSCOPED: + pass + else: + raise ValueError('Token needs to be scoped either to project or ' + 'a domain') + + data = json.dumps(data) + response = self.request('/v3/auth/tokens', data=data, + headers={'Content-Type': 'application/json'}, + method='POST') + + if response.status == httplib.UNAUTHORIZED: + # Invalid credentials + raise InvalidCredsError() + elif response.status in [httplib.OK, httplib.CREATED]: + headers = response.headers + + try: + body = json.loads(response.body) + except Exception: + e = sys.exc_info()[1] + raise MalformedResponseError('Failed to parse JSON', e) + + try: + roles = self._to_roles(body['token']['roles']) + except Exception: + e = sys.exc_info()[1] + roles = [] + + try: + expires = body['token']['expires_at'] + + self.auth_token = headers['x-subject-token'] + self.auth_token_expires = parse_date(expires) + # Note: catalog is not returned for unscoped tokens + self.urls = body['token'].get('catalog', None) + self.auth_user_info = None + self.auth_user_roles = roles + except KeyError: + e = sys.exc_info()[1] + raise MalformedResponseError('Auth JSON response is \ + missing required elements', e) + body = 'code: %s body:%s' % (response.status, response.body) + else: + raise MalformedResponseError('Malformed response', body=body, + driver=self.driver) + + return self + def get_class_for_auth_version(auth_version): """ Retrieve class for the provided auth version. @@ -1391,6 +1538,8 @@ def get_class_for_auth_version(auth_version): cls = OpenStackIdentity_2_0_Connection elif auth_version == '3.x_password': cls = OpenStackIdentity_3_0_Connection + elif auth_version == '3.x_oidc': + cls = OpenStackIdentity_3_0_Connection_OIDC else: raise LibcloudError('Unsupported Auth Version requested')