Skip to content
This repository has been archived by the owner on Dec 18, 2023. It is now read-only.

Commit

Permalink
Compatibility with KeyCloak authentication flow.
Browse files Browse the repository at this point in the history
  • Loading branch information
jochemb committed Jun 28, 2018
1 parent 07c7c7d commit e831b1f
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 56 deletions.
137 changes: 137 additions & 0 deletions transmart/api/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import abc
import requests
from getpass import getpass
import time


class Authenticator(metaclass=abc.ABCMeta):

def __init__(self, url, username=None, password=None, realm=None):
self.url = url
self.user = username or input('Username: ')
self.password = password
self.realm = realm
self._access_token = self.get_token()

@property
@abc.abstractmethod
def client_id(self):
pass

@abc.abstractmethod
def access_token(self) -> str:
pass

@abc.abstractmethod
def get_token(self) -> str:
pass

@abc.abstractmethod
def refresh(self):
pass


class LegacyAuth(Authenticator):
client_id = 'glowingbear-js'

@property
def access_token(self):
return self._access_token

def get_token(self):
r = requests.post(
"{}/oauth/token".format(self.url),
params=dict(
grant_type='password',
client_id=self.client_id,
username=self.user
),
data=dict(
password=self.password or getpass("Password: ")
)
)

r.raise_for_status()

return r.json().get('access_token')

def refresh(self):
self._access_token = self.get_token()


class KeyCloakAuth(Authenticator):
client_id = 'transmart-api'

def __init__(self, *args, **kwargs):
self.refresh_token = None
self.timeout = None
super().__init__(*args, **kwargs)

@property
def handle(self):
return '{}/auth/realms/{}/protocol/openid-connect/token'.format(self.url, self.realm)

@property
def access_token(self):
if self.timeout is not None and time.time() > self.timeout:
self.refresh()

return self._access_token

def get_token(self):
r = requests.post(
url=self.handle,
data=dict(
grant_type='password',
client_id=self.client_id,
username=self.user,
password=self.password or getpass("Password: "),
scope='offline'
)
)

r.raise_for_status()

if r.json().get('expires_in'):
self.timeout = time.time() + r.json().get('expires_in')

self.refresh_token = r.json().get('refresh_token')

return r.json().get('access_token')

def refresh(self):
r = requests.post(
url=self.handle,
data=dict(
grant_type='refresh_token',
refresh_token=self.refresh_token,
client_id=self.client_id,
)
)
if self.timeout is not None:
self.timeout = time.time() + r.json().get('expires_in')

self._access_token = r.json().get('access_token')


def get_auth(host, user=None, password=None, kc_url=None, kc_realm=None) -> Authenticator:
"""
Returns appropriate authenticator depending on the provided parameter.
If kc_url is provided returns the KeyCloakAuth, else LegacyAuth.
:param host: transmart api host.
:param user: username for authentication, will be asked if not provided.
:param password: password for authentication, will be asked if not provided.
:param kc_url: KeyCloak hostname (e.g. https://keycloak-test.thehyve.net)
:param kc_realm: Realm that is registered for the transmart api host to listen.
:return: Authenticator
"""

if kc_url:
return KeyCloakAuth(url=kc_url,
realm=kc_realm,
username=user,
password=password,
)
else:
return LegacyAuth(host, user, password)
33 changes: 0 additions & 33 deletions transmart/api/tm_api_base.py

This file was deleted.

31 changes: 18 additions & 13 deletions transmart/api/v1/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,26 @@
from .highdim_pb2 import Row
import urllib

from ..tm_api_base import TransmartAPIBase
from ..auth import get_auth


class TransmartV1(TransmartAPIBase):
""" Connect to tranSMART using Python. """
class TransmartV1:
""" Connect to tranSMART V1 api using Python. """

def __init__(self, host, user=None, password=None, print_urls=False):
def __init__(self, host, user=None, password=None, kc_url=None, kc_realm=None, print_urls=False):
"""
Create the python transmart client by providing user credentials.
:param host: a transmart URL (e.g. http://transmart-test.thehyve.net)
:param user: if not given, it asks for it.
:param password: if not given, it asks for it.
:param kc_url: KeyCloak hostname (e.g. https://keycloak-test.thehyve.net).
:param kc_realm: Realm that is registered for the transmart api host to listen.
:param print_urls: print the url of handles being used.
"""
super().__init__(host, user, password, print_urls)
self.host = host
self.print_urls = print_urls
self.auth = get_auth(host, user, password, kc_url, kc_realm)

def get_observations(self, study=None, patientSet=None, as_dataframe=True, hal=False):
"""
Expand Down Expand Up @@ -125,8 +129,8 @@ def _get_json_post(self, url, hal=False):

headers = {}
headers['Accept'] = 'application/%s;charset=UTF-8' % ('hal+json' if hal else 'json')
if self.access_token is not None:
headers['Authorization'] = 'Bearer ' + self.access_token
if self.auth.access_token is not None:
headers['Authorization'] = 'Bearer ' + self.auth.access_token
r = requests.post(url, headers=headers)
r.raise_for_status()
return r.json()
Expand All @@ -136,10 +140,11 @@ def _get_json(self, url, hal=False):
if self.print_urls:
print(url)

headers = {}
headers['Accept'] = 'application/%s;charset=UTF-8' % ('hal+json' if hal else 'json')
if self.access_token is not None:
headers['Authorization'] = 'Bearer ' + self.access_token
headers = {
'Accept': 'application/%s;charset=UTF-8' % ('hal+json' if hal else 'json')
}
if self.auth.access_token is not None:
headers['Authorization'] = 'Bearer ' + self.auth.access_token

r = requests.get(url, headers=headers)
r.raise_for_status()
Expand All @@ -165,8 +170,8 @@ def _get_protobuf(self, url):
headers = {
'Accept': 'application/octet-stream'
}
if self.access_token is not None:
headers['Authorization'] = 'Bearer ' + self.access_token
if self.auth.access_token is not None:
headers['Authorization'] = 'Bearer ' + self.auth.access_token
req = urllib.request.Request(url, headers=headers)
return self._parse_protobuf(urllib.request.urlopen(req).read())

Expand Down
19 changes: 12 additions & 7 deletions transmart/api/v2/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from .constraints import ObservationConstraint, Queryable, BiomarkerConstraint
from .data_structures import (ObservationSet, ObservationSetHD, TreeNodes, Patients,
PatientSets, Studies, StudyList, RelationTypes)
from ..tm_api_base import TransmartAPIBase
from ..auth import get_auth

logger = logging.getLogger('tm-api')

Expand Down Expand Up @@ -54,25 +54,30 @@ def headers(self):
return {'Accept': 'application/{};charset=UTF-8'.format('hal+json' if self.hal else 'json')}


class TransmartV2(TransmartAPIBase):
""" Connect to tranSMART using Python. """
class TransmartV2:
""" Connect to tranSMART v2 API using Python. """

def __init__(self, host, user=None, password=None, print_urls=False, interactive=True):
def __init__(self, host, user=None, password=None, kc_url=None, kc_realm=None, print_urls=False, interactive=True):
"""
Create the python transmart client by providing user credentials.
:param host: a transmart URL (e.g. http://transmart-test.thehyve.net)
:param user: if not given, it asks for it.
:param password: if not given, it asks for it.
:param kc_url: KeyCloak hostname (e.g. https://keycloak-test.thehyve.net)
:param kc_realm: Realm that is registered for the transmart api host to listen.
:param print_urls: print the url of handles being used.
:param interactive: automatically build caches for interactive use.
"""
super().__init__(host, user, password, print_urls)
self.studies = None
self.tree_dict = None
self.search_tree_node = None
self.relation_types = None
self.host = host
self.interactive = interactive
self.print_urls = print_urls

self.auth = get_auth(host, user, password, kc_url, kc_realm)

self._observation_call_factory('aggregates_per_concept')
self._observation_call_factory('counts')
Expand Down Expand Up @@ -101,7 +106,7 @@ def query(self, q):
url = "{}{}".format(self.host, q.handle)

headers = q.headers
headers['Authorization'] = 'Bearer ' + self.access_token
headers['Authorization'] = 'Bearer ' + self.auth.access_token

if q.method.upper() == 'GET':
r = requests.get(url, params=q.params, headers=headers)
Expand Down Expand Up @@ -237,7 +242,7 @@ def tree_nodes(self, root=None, depth=0, counts=False, tags=True, hal=False):

@default_constraint
@add_to_queryable
def get_hd_node_data(self, constraint=None, biomarker_constraint=None, biomarkers: list=None,
def get_hd_node_data(self, constraint=None, biomarker_constraint=None, biomarkers: list = None,
biomarker_type='genes', projection='all_data', **kwargs):
"""
:param constraint:
Expand Down
10 changes: 7 additions & 3 deletions transmart/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@
from .api.v2.api import TransmartV2


def get_api(host, api_version=2, user=None, password=None, print_urls=False, **kwargs):
def get_api(host, api_version=2, user=None, password=None, kc_url=None,
kc_realm=None, print_urls=False, interactive=None, **kwargs):
"""
Create the python transmart client by providing user credentials.
:param host: a transmart URL (e.g. http://transmart-test.thehyve.net)
:param api_version: either 1 or 2. Default is 2.
:param user: if not given, it asks for it.
:param password: if not given, it asks for it.
:param api_version: either 1 or 2. Default is 2.
:param kc_url: KeyCloak hostname (e.g. https://keycloak-test.thehyve.net)
:param kc_realm: Realm that is registered for the transmart api host to listen.
:param print_urls: print the url of handles being used.
:param interactive: automatically build caches for interactive use.
"""
api_versions = (1, 2)

Expand All @@ -19,4 +23,4 @@ def get_api(host, api_version=2, user=None, password=None, print_urls=False, **k

api = TransmartV1 if api_version == 1 else TransmartV2

return api(host, user=user, password=password, print_urls=print_urls, **kwargs)
return api(host, user, password, kc_url, kc_realm, print_urls, interactive, **kwargs)

0 comments on commit e831b1f

Please sign in to comment.