You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by ma...@apache.org on 2019/10/11 20:42:24 UTC

[airavata-custos] 17/24: added documentation for custos python SDK

This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-custos.git

commit 09977165fe06d2de81fa471660914e6fe7d9f4ee
Author: Aarushi <aa...@gmail.com>
AuthorDate: Sat Sep 14 17:50:48 2019 -0400

    added documentation for custos python SDK
---
 clients/python/README.md                           |  10 +-
 .../airavata_custos/admin/iam_admin_client.py      |  65 ++++++++----
 .../airavata_custos/security/client_credentials.py |  69 ++++++++-----
 ...tion_token.py => custos_authorization_token.py} |  13 ++-
 .../security/keycloak_connectors.py                | 113 ++++++++++++++++-----
 .../airavata_custos/security/model/__init__.py     |   0
 .../airavata_custos/security/model/ttypes.py       |  97 ------------------
 clients/python/airavata_custos/settings.py         |   8 +-
 clients/python/airavata_custos/utils.py            |  70 ++++++++++++-
 9 files changed, 258 insertions(+), 187 deletions(-)

diff --git a/clients/python/README.md b/clients/python/README.md
index 46690eb..907817d 100644
--- a/clients/python/README.md
+++ b/clients/python/README.md
@@ -1,8 +1,10 @@
 # Airavata Custos Python SDK
 
-####Create a virtual environment
+Create a virtual environment
+- python3 -m venv venv
 
-####Activate the virtual environment
+Activate the virtual environment
+- source venv/bin/activate
 
-####Install dependencies
-pip install -r requirements_dev.txt
\ No newline at end of file
+Install dependencies
+- pip install -r requirements_dev.txt
\ No newline at end of file
diff --git a/clients/python/airavata_custos/admin/iam_admin_client.py b/clients/python/airavata_custos/admin/iam_admin_client.py
index 6f83b58..ee9c11b 100644
--- a/clients/python/airavata_custos/admin/iam_admin_client.py
+++ b/clients/python/airavata_custos/admin/iam_admin_client.py
@@ -1,3 +1,20 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
 import logging
 from airavata_custos.utils import iamadmin_client_pool
 
@@ -71,41 +88,51 @@ def is_user_exist(authz_token, username):
     :param username: The username of the user
     :return: boolean
     """
-    return iamadmin_client_pool.isUserExist(authz_token, username)
+    try:
+        return iamadmin_client_pool.isUserExist(authz_token, username)
+    except Exception:
+        return None
 
 
 def get_user(authz_token, username):
     """
 
-    :param authz_token:
-    :param username:
-    :return:
+    :param authz_token: Object of AuthzToken class containing access token, username, gatewayId of the active user
+    :param username: username of the user
+    :return: object of class UserProfile
     """
-    return iamadmin_client_pool.getUser(authz_token, username)
+    try:
+        return iamadmin_client_pool.getUser(authz_token, username)
+    except Exception:
+        return None
 
 
-def get_users(authz_token, offset, limit, search=None):
+def get_users(authz_token, offset=0, limit=-1, search=None):
     """
 
-    :param authz_token:
-    :param offset:
-    :param limit:
-    :param search:
-    :return:
+    :param authz_token: Object of AuthzToken class containing access token, username, gatewayId of the active user
+    :param offset: start index
+    :param limit: end index
+    :param search: search criteria for filtering users
+    :return: list of UserProfile class objects
     """
-    return iamadmin_client_pool.getUsers(authz_token, offset, limit, search)
+    try:
+        return iamadmin_client_pool.getUsers(authz_token, offset, limit, search)
+    except Exception:
+        return None
 
 
 def reset_user_password(authz_token, username, new_password):
     """
 
-    :param authz_token:
-    :param username:
-    :param new_password:
+    :param authz_token: Object of AuthzToken class containing access token, username, gatewayId of the active user
+    :param username: username of the user
+    :param new_password: new password for the user
     :return:
     """
-    return iamadmin_client_pool.resetUserPassword(
-        authz_token, username, new_password)
+    try:
+        return iamadmin_client_pool.resetUserPassword(
+            authz_token, username, new_password)
+    except Exception:
+        return None
 
-def set_up_tenant(authz_token, gateway, tenantAdminPasswordCredentials):
-    pass
diff --git a/clients/python/airavata_custos/security/client_credentials.py b/clients/python/airavata_custos/security/client_credentials.py
index 432dc51..a0934d1 100644
--- a/clients/python/airavata_custos/security/client_credentials.py
+++ b/clients/python/airavata_custos/security/client_credentials.py
@@ -18,38 +18,51 @@
 
 class ClientCredentials(object):
     """
-
-    Attributes:
-        client_id:  Client ID, which you get from tenant registration with Keycloak
-        client_secret: Client Secret, which you get from tenant registration with Keycloak
-        username:  Username of the tenant user that needs to be authenticated
-        password:  Password of the tenant user that needs to be authenticated
-        authorization_code_url: URL of the authorization server’s authorization endpoint
-        state:
-        redirect_uri: Redirect URI you registered as callback
-        refresh_token:
-        verify_ssl: Flag to indicate ssl verification is required
-
+    This is the base class for passing parameters required to authenticate with keycloak
     """
-    client_id = None
-    client_secret = None
-    verify_ssl = None
-    authorization_code_url = None
-    state = None
-    redirect_uri = None
-    username = None
-    password = None
-    refresh_token = None
-
-    def __init__(self, client_id, client_secret, verify_ssl=False, authorization_code_url=None, state=None, redirect_uri=None, username=None, password=None, refresh_token=None):
+    def __init__(self, client_id, client_secret):
+        """
+        This is the constructor for ClientCredentials class
+        :param client_id: client identifier received after registering the tenant
+        :param client_secret: client password received after registering the tenant
+        """
         self.client_id = client_id
         self.client_secret = client_secret
-        self.verify_ssl = verify_ssl
-        self.authorization_code_url = authorization_code_url
-        self.state = state
-        self.redirect_uri = redirect_uri
+
+
+class UserCredentials(ClientCredentials):
+    """
+    This class inherits from ClientCredentials class. Used for passing parameters required to authenticate user
+    with keycloak
+    """
+    def __init__(self, client_id, client_secret, username, password):
+        """
+        This is the constructor for UserCredentials class
+        :param client_id: client identifier received after registering the tenant
+        :param client_secret: client password received after registering the tenant
+        :param username: username of the user which needs to be authenticated
+        :param password: password of the user which needs to be authenticated
+        """
+        super().__init__(client_id, client_secret)
         self.username = username
         self.password = password
-        self.refresh_token = refresh_token
 
 
+class AccountCredentials(ClientCredentials):
+    """
+    This class inherits from ClientCredentials class. Used for passing parameters required to authenticate service
+    account with keycloak
+    """
+    def __init__(self, client_id, client_secret, authorization_code_url, state, redirect_uri):
+        """
+        This is the constructor for AccountCredentials class
+        :param client_id: client identifier received after registering the tenant
+        :param client_secret: client password received after registering the tenant
+        :param authorization_code_url: The URL that the user will be redirected back from the keycloak to the client
+        :param state: An state string for CSRF protection.
+        :param redirect_uri: URI for the callback entry point of the client
+        """
+        super().__init__(client_id, client_secret)
+        self.authorization_code_url = authorization_code_url
+        self.state = state
+        self.redirect_uri = redirect_uri
diff --git a/clients/python/airavata_custos/security/authorization_token.py b/clients/python/airavata_custos/security/custos_authorization_token.py
similarity index 73%
rename from clients/python/airavata_custos/security/authorization_token.py
rename to clients/python/airavata_custos/security/custos_authorization_token.py
index cbe4fe2..dd4b531 100644
--- a/clients/python/airavata_custos/security/authorization_token.py
+++ b/clients/python/airavata_custos/security/custos_authorization_token.py
@@ -17,10 +17,18 @@
 from airavata_custos import settings
 from oauthlib.oauth2 import BackendApplicationClient
 from requests_oauthlib import OAuth2Session
-from airavata_custos.security.model.ttypes import AuthzToken
+from custos.commons.model.security.ttypes import AuthzToken
 
 
-def create_authorization_token(client_credentials, tenant_id, username=None):
+def get_authorization_token(client_credentials, tenant_id, username=None):
+    """
+    This method created a authorization token for the user or a service account
+    In case of a service account username will be null
+    :param client_credentials: object of class client_credentials
+    :param tenant_id: gateway id of the client
+    :param username: username of the user for which authorization token is being created
+    :return: AuthzToken
+    """
     client = BackendApplicationClient(client_id=client_credentials.client_id)
     oauth = OAuth2Session(client=client)
     token = oauth.fetch_token(
@@ -32,5 +40,4 @@ def create_authorization_token(client_credentials, tenant_id, username=None):
     access_token = token.get('access_token')
     return AuthzToken(
         accessToken=access_token,
-        # This is a service account, so leaving out userName for now
         claimsMap={'gatewayID': tenant_id, 'userName': username})
diff --git a/clients/python/airavata_custos/security/keycloak_connectors.py b/clients/python/airavata_custos/security/keycloak_connectors.py
index 054c787..d31e5a8 100644
--- a/clients/python/airavata_custos/security/keycloak_connectors.py
+++ b/clients/python/airavata_custos/security/keycloak_connectors.py
@@ -14,6 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
+import time
 from oauthlib.oauth2 import LegacyApplicationClient
 from requests_oauthlib import OAuth2Session
 import requests
@@ -21,29 +22,46 @@ from airavata_custos import settings
 
 
 class KeycloakBackend(object):
-    def authenticate(self, client_credentials):
-        """This method authenticates a client with keycloak
 
-        Parameters:
-        client_credentials (client_credentials): This object has client credentials which needs to be authenticated
+    def authenticate_user(self, user_credentials):
+        """
+        Method to authenticate a gateway user with keycloak
+        :param user_credentials: object of UserCredentials class
+        :return: Token object, UserInfo object
+        """
+        try:
+            token, user_info = self._get_token_and_user_info_password_flow(user_credentials)
+            return token, user_info
+        except Exception as e:
+            return None
+
+    def authenticate_account(self, account_credentials):
+        """
+
+        :param account_credentials: object of AccountCredentials class
+        :return: Token object, UserInfo object
+        """
+        try:
+            token, user_info = self._get_token_and_user_info_redirect_flow(account_credentials)
+            return token, user_info
+        except Exception as e:
+            return None
 
-        Returns:
-        String:token
-        String:userInfo
-       """
+    def authenticate_using_refresh_token(self, client_credentials, refresh_token):
+        """
+
+        :param client_credentials: object of ClientCredentials class
+        :param refresh_token: openid connect refresh token
+        :return: Token object
+        """
         try:
-            if client_credentials.username and client_credentials.password:
-                token, userinfo = self._get_token_and_userinfo_password_flow(client_credentials)
-            elif client_credentials.refresh_token:
-                token = self._get_token_from_refresh_token(client_credentials)
-            elif client_credentials.red:
-                token, userinfo = self._get_token_and_userinfo_redirect_flow(client_credentials)
-
-            return token, userinfo
+            token = self._get_token_from_refresh_token(client_credentials, refresh_token)
+            return token
         except Exception as e:
             return None
 
-    def _get_token_and_userinfo_password_flow(self, client_credentials):
+    @classmethod
+    def _get_token_and_user_info_password_flow(cls, client_credentials):
 
         oauth2_session = OAuth2Session(client=LegacyApplicationClient(client_id=client_credentials.client_id))
         token = oauth2_session.fetch_token(token_url=settings.KEYCLOAK_TOKEN_URL,
@@ -51,11 +69,12 @@ class KeycloakBackend(object):
                                            password=client_credentials.password,
                                            client_id=client_credentials.client_id,
                                            client_secret=client_credentials.client_secret,
-                                           verify=client_credentials.verify_ssl)
-        userinfo = oauth2_session.get(settings.KEYCLOAK_USERINFO_URL).json()
-        return token, userinfo
+                                           verify=settings.VERIFY_SSL)
+        user_info = oauth2_session.get(settings.KEYCLOAK_USERINFO_URL).json()
+        return cls._process_token(token), cls._process_userinfo(user_info)
 
-    def _get_token_and_userinfo_redirect_flow(self, client_credentials):
+    @classmethod
+    def _get_token_and_user_info_redirect_flow(cls, client_credentials):
         oauth2_session = OAuth2Session(client_credentials.client_id,
                                        scope='openid',
                                        redirect_uri=client_credentials.redirect_uri,
@@ -63,16 +82,54 @@ class KeycloakBackend(object):
         token = oauth2_session.fetch_token(settings.KEYCLOAK_TOKEN_URL,
                                            client_secret=client_credentials.client_secret,
                                            authorization_response=client_credentials.authorization_code_url,
-                                           verify=client_credentials.verify_ssl)
-        userinfo = oauth2_session.get(settings.KEYCLOAK_USERINFO_URL).json()
-        return token, userinfo
+                                           verify=settings.VERIFY_SSL)
+        user_info = oauth2_session.get(settings.KEYCLOAK_USERINFO_URL).json()
+        return cls._process_token(token), cls._process_userinfo(user_info)
 
-    def _get_token_from_refresh_token(self, client_credentials):
+    @classmethod
+    def _get_token_from_refresh_token(cls, client_credentials, refresh_token):
 
         oauth2_session = OAuth2Session(client_credentials.client_id, scope='openid')
         auth = requests.auth.HTTPBasicAuth(client_credentials.client_id, client_credentials.client_secret)
         token = oauth2_session.refresh_token(token_url=settings.KEYCLOAK_TOKEN_URL,
-                                             refresh_token=client_credentials.refresh_token,
+                                             refresh_token=refresh_token,
                                              auth=auth,
-                                             verify=client_credentials.verify_ssl)
-        return token
\ No newline at end of file
+                                             verify=settings.VERIFY_SSL)
+        return cls._process_token(token)
+
+    @classmethod
+    def _process_token(cls, token):
+
+        now = time.time()
+        access_token = token['access_token']
+        access_token_expires_at = now + token['expires_in']
+        refresh_token = token['refresh_token']
+        refresh_token_expires_at = now + token['refresh_expires_in']
+        return Token(access_token, access_token_expires_at, refresh_token, refresh_token_expires_at)
+
+    @classmethod
+    def _process_userinfo(cls, userinfo):
+
+        username = userinfo['preferred_username']
+        email = userinfo['email']
+        first_name = userinfo['given_name']
+        last_name = userinfo['family_name']
+        return UserInfo(username, email, first_name, last_name)
+
+
+class Token(object):
+
+    def __init__(self, access_token, access_token_expires_at, refresh_token, refresh_token_expires_at):
+        self.access_token = access_token
+        self.access_token_expires_at = access_token_expires_at
+        self.refresh_token = refresh_token
+        self.refresh_token_expires_at = refresh_token_expires_at
+
+
+class UserInfo(object):
+
+    def __init__(self, username, email, first_name, last_name):
+        self.username = username
+        self.email = email
+        self.first_name = first_name
+        self.last_name = last_name
diff --git a/clients/python/airavata_custos/security/model/__init__.py b/clients/python/airavata_custos/security/model/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/clients/python/airavata_custos/security/model/ttypes.py b/clients/python/airavata_custos/security/model/ttypes.py
deleted file mode 100644
index 596f6ed..0000000
--- a/clients/python/airavata_custos/security/model/ttypes.py
+++ /dev/null
@@ -1,97 +0,0 @@
-#
-# Autogenerated by Thrift Compiler (0.10.0)
-#
-# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
-#
-#  options string: py
-#
-
-from thrift.Thrift import TType, TMessageType, TFrozenDict, TException, TApplicationException
-from thrift.protocol.TProtocol import TProtocolException
-import sys
-
-from thrift.transport import TTransport
-
-
-class AuthzToken(object):
-    """
-    Attributes:
-     - accessToken
-     - claimsMap
-    """
-
-    thrift_spec = (
-        None,  # 0
-        (1, TType.STRING, 'accessToken', 'UTF8', None, ),  # 1
-        (2, TType.MAP, 'claimsMap', (TType.STRING, 'UTF8', TType.STRING, 'UTF8', False), None, ),  # 2
-    )
-
-    def __init__(self, accessToken=None, claimsMap=None,):
-        self.accessToken = accessToken
-        self.claimsMap = claimsMap
-
-    def read(self, iprot):
-        if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None:
-            iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec))
-            return
-        iprot.readStructBegin()
-        while True:
-            (fname, ftype, fid) = iprot.readFieldBegin()
-            if ftype == TType.STOP:
-                break
-            if fid == 1:
-                if ftype == TType.STRING:
-                    self.accessToken = iprot.readString().decode('utf-8') if sys.version_info[0] == 2 else iprot.readString()
-                else:
-                    iprot.skip(ftype)
-            elif fid == 2:
-                if ftype == TType.MAP:
-                    self.claimsMap = {}
-                    (_ktype1, _vtype2, _size0) = iprot.readMapBegin()
-                    for _i4 in range(_size0):
-                        _key5 = iprot.readString().decode('utf-8') if sys.version_info[0] == 2 else iprot.readString()
-                        _val6 = iprot.readString().decode('utf-8') if sys.version_info[0] == 2 else iprot.readString()
-                        self.claimsMap[_key5] = _val6
-                    iprot.readMapEnd()
-                else:
-                    iprot.skip(ftype)
-            else:
-                iprot.skip(ftype)
-            iprot.readFieldEnd()
-        iprot.readStructEnd()
-
-    def write(self, oprot):
-        if oprot._fast_encode is not None and self.thrift_spec is not None:
-            oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec)))
-            return
-        oprot.writeStructBegin('AuthzToken')
-        if self.accessToken is not None:
-            oprot.writeFieldBegin('accessToken', TType.STRING, 1)
-            oprot.writeString(self.accessToken.encode('utf-8') if sys.version_info[0] == 2 else self.accessToken)
-            oprot.writeFieldEnd()
-        if self.claimsMap is not None:
-            oprot.writeFieldBegin('claimsMap', TType.MAP, 2)
-            oprot.writeMapBegin(TType.STRING, TType.STRING, len(self.claimsMap))
-            for kiter7, viter8 in self.claimsMap.items():
-                oprot.writeString(kiter7.encode('utf-8') if sys.version_info[0] == 2 else kiter7)
-                oprot.writeString(viter8.encode('utf-8') if sys.version_info[0] == 2 else viter8)
-            oprot.writeMapEnd()
-            oprot.writeFieldEnd()
-        oprot.writeFieldStop()
-        oprot.writeStructEnd()
-
-    def validate(self):
-        if self.accessToken is None:
-            raise TProtocolException(message='Required field accessToken is unset!')
-        return
-
-    def __repr__(self):
-        L = ['%s=%r' % (key, value)
-             for key, value in self.__dict__.items()]
-        return '%s(%s)' % (self.__class__.__name__, ', '.join(L))
-
-    def __eq__(self, other):
-        return isinstance(other, self.__class__) and self.__dict__ == other.__dict__
-
-    def __ne__(self, other):
-        return not (self == other)
diff --git a/clients/python/airavata_custos/settings.py b/clients/python/airavata_custos/settings.py
index c36909e..89a05ef 100644
--- a/clients/python/airavata_custos/settings.py
+++ b/clients/python/airavata_custos/settings.py
@@ -26,6 +26,8 @@ KEYCLOAK_LOGOUT_URL = 'https://localhost:8443/auth/realms/default/protocol/openi
 THRIFT_CLIENT_POOL_KEEPALIVE = 5
 
 # Profile Service Configuration
-PROFILE_SERVICE_HOST = ''
-PROFILE_SERVICE_PORT = ''
-PROFILE_SERVICE_SECURE = False
\ No newline at end of file
+PROFILE_SERVICE_HOST = '0.0.0.0'
+PROFILE_SERVICE_PORT = '8081'
+PROFILE_SERVICE_SECURE = False
+
+VERIFY_SSL = False
diff --git a/clients/python/airavata_custos/utils.py b/clients/python/airavata_custos/utils.py
index afad780..17fdf59 100644
--- a/clients/python/airavata_custos/utils.py
+++ b/clients/python/airavata_custos/utils.py
@@ -1,6 +1,70 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import logging
 import thrift_connector.connection_pool as connection_pool
+from thrift.protocol import TBinaryProtocol
+from thrift.protocol.TMultiplexedProtocol import TMultiplexedProtocol
+from thrift.transport import TSocket, TSSLSocket, TTransport
+
 from airavata_custos import settings
-from custos.service.profile.iam.admin.services.cpi import IamAdminServices, constants
+from custos.profile.iam.admin.services.cpi import IamAdminServices, constants
+
+log = logging.getLogger(__name__)
+
+
+class MultiplexThriftClientMixin:
+    service_name = None
+
+    @classmethod
+    def get_protoco_factory(cls):
+        def factory(transport):
+            protocol = TBinaryProtocol.TBinaryProtocol(transport)
+            multiplex_prot = TMultiplexedProtocol(protocol, cls.service_name)
+            return multiplex_prot
+        return factory
+
+
+class CustomThriftClient(connection_pool.ThriftClient):
+    secure = False
+    validate = False
+
+    @classmethod
+    def get_socket_factory(cls):
+        if not cls.secure:
+            return super().get_socket_factory()
+        else:
+            def factory(host, port):
+                return TSSLSocket.TSSLSocket(host, port, validate=cls.validate)
+            return factory
+
+    def ping(self):
+        try:
+            self.client.getAPIVersion()
+        except Exception as e:
+            log.debug("getAPIVersion failed: {}".format(str(e)))
+            raise
+
+
+class IAMAdminServiceThriftClient(MultiplexThriftClientMixin,
+                                  CustomThriftClient):
+    service_name = constants.IAM_ADMIN_SERVICES_CPI_NAME
+    secure = settings.PROFILE_SERVICE_SECURE
+
 
 iamadmin_client_pool = connection_pool.ClientPool(
     IamAdminServices,
@@ -10,7 +74,3 @@ iamadmin_client_pool = connection_pool.ClientPool(
     keepalive=settings.THRIFT_CLIENT_POOL_KEEPALIVE
 )
 
-class IAMAdminServiceThriftClient(MultiplexThriftClientMixin,
-                                  CustomThriftClient):
-    service_name = constants.IAM_ADMIN_SERVICES_CPI_NAME
-    secure = settings.PROFILE_SERVICE_SECURE
\ No newline at end of file