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')
 

Reply via email to