33import json
44import os
55from abc import ABC
6- from typing import Any , Dict , List , Tuple , Union
6+ from typing import Any , Dict , List , Optional , Tuple , Union
77
88import requests
99
1010import logging
1111
1212logger = logging .getLogger (__name__ )
1313
14+ @dataclass
15+ class KeycloakSettings :
16+ """
17+ Keycloak settings for OIDC token retrieval.
18+
19+ If a field is not provided, it falls back to environment variables and then
20+ the same defaults used by `webapp/scripts/load_examples.py`.
21+ """
22+
23+ keycloak_url : Optional [str ] = None
24+ realm : Optional [str ] = None
25+ client_id : Optional [str ] = None
26+ client_secret : Optional [str ] = None
27+ username : Optional [str ] = None
28+ password : Optional [str ] = None
29+ scope : str = "openid profile email"
30+
31+ def __post_init__ (self ):
32+ self .keycloak_url = self .keycloak_url or os .environ .get ("KEYCLOAK_URL" , "http://keycloak.cogstack.localhost" )
33+ self .realm = self .realm or os .environ .get ("KEYCLOAK_REALM" , "cogstack-realm" )
34+ self .client_id = self .client_id or os .environ .get ("KEYCLOAK_CLIENT_ID" , "cogstack-medcattrainer-frontend" )
35+ self .client_secret = self .client_secret or os .environ .get ("KEYCLOAK_CLIENT_SECRET" )
36+ self .username = self .username or os .environ .get ("KEYCLOAK_USERNAME" , "admin" )
37+ self .password = self .password or os .environ .get ("KEYCLOAK_PASSWORD" , "admin" )
38+
39+
40+ def get_keycloak_access_token (settings : KeycloakSettings ) -> str :
41+ token_url = f"{ settings .keycloak_url } /realms/{ settings .realm } /protocol/openid-connect/token"
42+ if settings .client_secret :
43+ data = {
44+ "grant_type" : "client_credentials" ,
45+ "client_id" : settings .client_id ,
46+ "client_secret" : settings .client_secret ,
47+ "scope" : settings .scope ,
48+ }
49+ else :
50+ data = {
51+ "grant_type" : "password" ,
52+ "client_id" : settings .client_id ,
53+ "username" : settings .username ,
54+ "password" : settings .password ,
55+ "scope" : settings .scope ,
56+ }
57+ try :
58+ logger .debug (f"Getting Keycloak token from { token_url } " )
59+ resp = requests .post (token_url , data = data )
60+ resp .raise_for_status ()
61+ return resp .json ()["access_token" ]
62+ except Exception as e :
63+ raise MCTUtilsException ("Failed to get Keycloak access token" , e )
64+
1465
1566@dataclass
1667class MCTObj (ABC ):
@@ -224,7 +275,7 @@ class MedCATTrainerSession:
224275 >>> annotations = session.get_project_annos(projects[0])
225276 """
226277
227- def __init__ (self , server = None , username = None , password = None ):
278+ def __init__ (self , server = None , username = None , password = None , keycloak_settings = None , use_oidc : bool = False ):
228279 """Initialize the MedCATTrainerSession.
229280
230281 Args:
@@ -237,15 +288,31 @@ def __init__(self, server=None, username=None, password=None):
237288 self .password = password or os .getenv ("MCTRAINER_PASSWORD" )
238289 self .server = server or 'http://localhost:8001'
239290
291+ env_use_oidc = os .getenv ("MCTRAINER_USE_OIDC" , "" )
292+ env_use_oidc_truthy = env_use_oidc .strip () == "1"
293+ effective_use_oidc = bool (use_oidc ) or bool (env_use_oidc_truthy )
294+
295+ if effective_use_oidc :
296+ if keycloak_settings is None :
297+ kc_settings = KeycloakSettings ()
298+ else :
299+ if not isinstance (keycloak_settings , KeycloakSettings ):
300+ raise TypeError ("keycloak_settings must be a KeycloakSettings instance" )
301+ kc_settings = keycloak_settings
302+
303+ token = get_keycloak_access_token (kc_settings )
304+ self .headers = {"Authorization" : f"Bearer { token } " }
305+ return
306+
240307 payload = {"username" : self .username , "password" : self .password }
241308 resp = requests .post (f"{ self .server } /api/api-token-auth/" , json = payload )
242309 if 200 <= resp .status_code < 300 :
243310 token = json .loads (resp .text )["token" ]
244311 self .headers = {
245- ' Authorization' : f' Token { token } ' ,
312+ " Authorization" : f" Token { token } " ,
246313 }
247314 else :
248- raise MCTUtilsException (f' Failed to login to MedCATtrainer instance running at: { self .server } ' )
315+ raise MCTUtilsException (f" Failed to login to MedCATtrainer instance running at: { self .server } " )
249316
250317 def create_project (self , name : str ,
251318 description : str ,
@@ -482,8 +549,23 @@ def get_users(self) -> List[MCTUser]:
482549 Returns:
483550 List[MCTUser]: A list of all users in the MedCATTrainer instance
484551 """
485- users = json .loads (requests .get (f'{ self .server } /api/users/' , headers = self .headers ).text )['results' ]
486- return [MCTUser (id = u ['id' ], username = u ['username' ]) for u in users ]
552+ resp = requests .get (f"{ self .server } /api/users/" , headers = self .headers )
553+ if not (200 <= resp .status_code < 300 ):
554+ raise MCTUtilsException (
555+ f"Failed to get users from MedCATTrainer instance running at: { self .server } " ,
556+ f"HTTP { resp .status_code } : { resp .text [:500 ]} " ,
557+ )
558+ try :
559+ payload = resp .json ()
560+ if not isinstance (payload , dict ):
561+ payload = json .loads (resp .text or "{}" )
562+ except Exception as e :
563+ raise MCTUtilsException (
564+ f"Failed to parse users response from MedCATTrainer instance running at: { self .server } " ,
565+ e ,
566+ )
567+ users = payload .get ("results" , [])
568+ return [MCTUser (id = u .get ("id" ), username = u .get ("username" )) for u in users ]
487569
488570 def get_models (self ) -> Tuple [List [str ], List [str ]]:
489571 """Get all MedCAT cdb and vocab models in the MedCATTrainer instance.
0 commit comments