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