30
30
from ..exceptions import LoginError
31
31
from ..utils import commons
32
32
from ..utils .process_asyncs import async_to_sync
33
- from ..query import QueryWithLogin
33
+ from ..query import BaseQuery , QueryWithLogin
34
34
from .tapsql import _gen_pos_sql , _gen_str_sql , _gen_numeric_sql ,\
35
35
_gen_band_list_sql , _gen_datetime_sql , _gen_pol_sql , _gen_pub_sql ,\
36
36
_gen_science_sql , _gen_spec_res_sql , ALMA_DATE_FORMAT
@@ -212,6 +212,101 @@ def _gen_sql(payload):
212
212
return sql + where
213
213
214
214
215
+ class AlmaAuth (BaseQuery ):
216
+ """Authentication session information for passing credentials to an OIDC instance
217
+
218
+ Assumes an OIDC system like Keycloak with a preconfigured client app called "oidc" to validate against.
219
+ This does not use Tokens in the traditional OIDC sense, but rather uses the Keycloak specific endpoint
220
+ to validate a username and password. Passwords are then kept in a Python keyring.
221
+ """
222
+
223
+ _CLIENT_ID = 'oidc'
224
+ _GRANT_TYPE = 'password'
225
+ _INVALID_PASSWORD_MESSAGE = 'Invalid user credentials'
226
+ _REALM_ENDPOINT = '/auth/realms/ALMA'
227
+ _LOGIN_ENDPOINT = f'{ _REALM_ENDPOINT } /protocol/openid-connect/token'
228
+ _VERIFY_WELL_KNOWN_ENDPOINT = f'{ _REALM_ENDPOINT } /.well-known/openid-configuration'
229
+
230
+ def __init__ (self ):
231
+ super ().__init__ ()
232
+ self ._auth_hosts = auth_urls
233
+ self ._auth_host = None
234
+
235
+ @property
236
+ def auth_hosts (self ):
237
+ return self ._auth_hosts
238
+
239
+ @auth_hosts .setter
240
+ def auth_hosts (self , auth_hosts ):
241
+ """
242
+ Set the available hosts to check for login endpoints.
243
+
244
+ Parameters
245
+ ----------
246
+ auth_hosts : array
247
+ Available hosts name. Checking each one until one returns a 200 for
248
+ the well-known endpoint.
249
+ """
250
+ if auth_hosts is None :
251
+ raise LoginError ('Valid authentication hosts cannot be None' )
252
+ else :
253
+ self ._auth_hosts = auth_hosts
254
+
255
+ def get_valid_host (self ):
256
+ if self ._auth_host is None :
257
+ for auth_url in self ._auth_hosts :
258
+ # set session cookies (they do not get set otherwise)
259
+ url_to_check = f'https://{ auth_url } { self ._VERIFY_WELL_KNOWN_ENDPOINT } '
260
+ response = self ._request ("HEAD" , url_to_check , cache = False )
261
+
262
+ if response .status_code == 200 :
263
+ self ._auth_host = auth_url
264
+ log .debug (f'Set auth host to { self ._auth_host } ' )
265
+ break
266
+
267
+ if self ._auth_host is None :
268
+ raise LoginError (f'No useable hosts to login to: { self ._auth_hosts } ' )
269
+ else :
270
+ return self ._auth_host
271
+
272
+ def login (self , username , password ):
273
+ """
274
+ Authenticate to one of the configured hosts.
275
+
276
+ Parameters
277
+ ----------
278
+ username : str
279
+ The username to authenticate with
280
+ password : str
281
+ The user's password
282
+ """
283
+ data = {
284
+ 'username' : username ,
285
+ 'password' : password ,
286
+ 'grant_type' : self ._GRANT_TYPE ,
287
+ 'client_id' : self ._CLIENT_ID
288
+ }
289
+
290
+ login_url = f'https://{ self .get_valid_host ()} { self ._LOGIN_ENDPOINT } '
291
+ log .info (f'Authenticating { username } on { login_url } .' )
292
+ login_response = self ._request ('POST' , login_url , data = data , cache = False )
293
+ json_auth = login_response .json ()
294
+
295
+ if 'error' in json_auth :
296
+ log .debug (f'{ json_auth } ' )
297
+ error_message = json_auth ['error_description' ]
298
+ if self ._INVALID_PASSWORD_MESSAGE not in error_message :
299
+ raise LoginError ("Could not log in to ALMA authorization portal: "
300
+ f"{ self .get_valid_host ()} Message from server: { error_message } " )
301
+ else :
302
+ raise LoginError (error_message )
303
+ elif 'access_token' not in json_auth :
304
+ raise LoginError ("Could not log in to any of the known ALMA authorization portals: \n "
305
+ f"No error from server, but missing access token from host: { self .get_valid_host ()} " )
306
+ else :
307
+ log .info (f'Successfully logged in to { self ._auth_host } ' )
308
+
309
+
215
310
@async_to_sync
216
311
class AlmaClass (QueryWithLogin ):
217
312
@@ -228,6 +323,11 @@ def __init__(self):
228
323
self ._sia_url = None
229
324
self ._tap_url = None
230
325
self ._datalink_url = None
326
+ self ._auth = AlmaAuth ()
327
+
328
+ @property
329
+ def auth (self ):
330
+ return self ._auth
231
331
232
332
@property
233
333
def datalink (self ):
@@ -875,11 +975,7 @@ def _get_auth_info(self, username, *, store_password=False,
875
975
else :
876
976
username = self .USERNAME
877
977
878
- if hasattr (self , '_auth_url' ):
879
- auth_url = self ._auth_url
880
- else :
881
- raise LoginError ("Login with .login() to acquire the appropriate"
882
- " login URL" )
978
+ auth_url = self .auth .get_valid_host ()
883
979
884
980
# Get password from keyring or prompt
885
981
password , password_from_keyring = self ._get_password (
@@ -909,69 +1005,16 @@ def _login(self, username=None, store_password=False,
909
1005
on the keyring. Default is False.
910
1006
"""
911
1007
912
- success = False
913
- for auth_url in auth_urls :
914
- # set session cookies (they do not get set otherwise)
915
- cookiesetpage = self ._request ("GET" ,
916
- urljoin (self ._get_dataarchive_url (),
917
- 'rh/forceAuthentication' ),
918
- cache = False )
919
- self ._login_cookiepage = cookiesetpage
920
- cookiesetpage .raise_for_status ()
921
-
922
- if (auth_url + '/cas/login' in cookiesetpage .request .url ):
923
- # we've hit a target, we're good
924
- success = True
925
- break
926
- if not success :
927
- raise LoginError ("Could not log in to any of the known ALMA "
928
- "authorization portals: {0}" .format (auth_urls ))
929
-
930
- # Check if already logged in
931
- loginpage = self ._request ("GET" , "https://{auth_url}/cas/login" .format (auth_url = auth_url ),
932
- cache = False )
933
- root = BeautifulSoup (loginpage .content , 'html5lib' )
934
- if root .find ('div' , class_ = 'success' ):
935
- log .info ("Already logged in." )
936
- return True
937
-
938
- self ._auth_url = auth_url
1008
+ self .auth .auth_hosts = auth_urls
939
1009
940
1010
username , password = self ._get_auth_info (username = username ,
941
1011
store_password = store_password ,
942
1012
reenter_password = reenter_password )
943
1013
944
- # Authenticate
945
- log .info ("Authenticating {0} on {1} ..." .format (username , auth_url ))
946
- # Do not cache pieces of the login process
947
- data = {kw : root .find ('input' , {'name' : kw })['value' ]
948
- for kw in ('execution' , '_eventId' )}
949
- data ['username' ] = username
950
- data ['password' ] = password
951
- data ['submit' ] = 'LOGIN'
952
-
953
- login_response = self ._request ("POST" , "https://{0}/cas/login" .format (auth_url ),
954
- params = {'service' : self ._get_dataarchive_url ()},
955
- data = data ,
956
- cache = False )
957
-
958
- # save the login response for debugging purposes
959
- self ._login_response = login_response
960
- # do not expose password back to user
961
- del data ['password' ]
962
- # but save the parameters for debug purposes
963
- self ._login_parameters = data
964
-
965
- authenticated = ('You have successfully logged in' in
966
- login_response .text )
967
-
968
- if authenticated :
969
- log .info ("Authentication successful!" )
970
- self .USERNAME = username
971
- else :
972
- log .exception ("Authentication failed!" )
1014
+ self .auth .login (username , password )
1015
+ self .USERNAME = username
973
1016
974
- return authenticated
1017
+ return True
975
1018
976
1019
def get_cycle0_uid_contents (self , uid ):
977
1020
"""
0 commit comments