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,92 @@ def _gen_sql(payload):
212
212
return sql + where
213
213
214
214
215
+ #
216
+ # Authentication session information for connecting to an OIDC instance. Assumes an OIDC system like Keycloak
217
+ # with a preconfigured client called "oidc".
218
+ #
219
+ class AlmaAuth (BaseQuery ):
220
+ _CLIENT_ID = 'oidc'
221
+ _GRANT_TYPE = 'password'
222
+ _INVALID_PASSWORD_MESSAGE = 'Invalid user credentials'
223
+ _REALM_ENDPOINT = '/auth/realms/ALMA'
224
+ _LOGIN_ENDPOINT = f'{ _REALM_ENDPOINT } /protocol/openid-connect/token'
225
+ _VERIFY_WELL_KNOWN_ENDPOINT = f'{ _REALM_ENDPOINT } /.well-known/openid-configuration'
226
+
227
+ def __init__ (self ):
228
+ super ().__init__ ()
229
+ self ._auth_hosts = auth_urls
230
+ self ._auth_host = None
231
+
232
+
233
+ def set_auth_hosts (self , auth_hosts ):
234
+ """
235
+ Set the available hosts to check for login endpoints.
236
+
237
+ Parameters
238
+ ----------
239
+ auth_hosts : array
240
+ Available hosts name. Checking each one until one returns a 200 for
241
+ the well-known endpoint.
242
+ """
243
+ self ._auth_hosts = auth_hosts
244
+
245
+ @property
246
+ def host (self ):
247
+ if self ._auth_host is None :
248
+ for auth_url in self ._auth_hosts :
249
+ # set session cookies (they do not get set otherwise)
250
+ url_to_check = f'https://{ auth_url } { self ._VERIFY_WELL_KNOWN_ENDPOINT } '
251
+ response = self ._request ("HEAD" , url_to_check , cache = False )
252
+
253
+ if response .status_code == 200 :
254
+ self ._auth_host = auth_url
255
+ log .debug (f'Set auth host to { self ._auth_host } ' )
256
+ break
257
+
258
+ if self ._auth_host is None :
259
+ raise LoginError (f'No useable hosts to login to: { self ._auth_hosts } ' )
260
+ else :
261
+ return self ._auth_host
262
+
263
+ def login (self , username , password ):
264
+ """
265
+ Authenticate to one of the configured hosts.
266
+
267
+ Parameters
268
+ ----------
269
+ username : str
270
+ The username to authenticate with
271
+ password : str
272
+ The user's password
273
+ """
274
+ data = {
275
+ 'username' : username ,
276
+ 'password' : password ,
277
+ 'grant_type' : self ._GRANT_TYPE ,
278
+ 'client_id' : self ._CLIENT_ID
279
+ }
280
+
281
+ login_url = f'https://{ self .host } { self ._LOGIN_ENDPOINT } '
282
+ log .info (f'Authenticating { username } on { login_url } .' )
283
+ login_response = self ._request ('POST' , login_url , data = data , cache = False )
284
+ json_auth = login_response .json ()
285
+
286
+ if 'error' in json_auth :
287
+ log .debug (f'{ json_auth } ' )
288
+ error_message = json_auth ['error_description' ]
289
+ if self ._INVALID_PASSWORD_MESSAGE not in error_message :
290
+ raise LoginError ("Could not log in to ALMA authorization portal: "
291
+ f"{ self .host } Message from server: { error_message } " )
292
+ else :
293
+ log .error (error_message )
294
+ elif 'access_token' not in json_auth :
295
+ raise LoginError ("Could not log in to any of the known ALMA authorization portals: \n "
296
+ f"No error from server, but missing access token from host: { self .host } " )
297
+ else :
298
+ log .info (f'Successfully logged in to { self ._auth_host } ' )
299
+
300
+
215
301
@async_to_sync
216
302
class AlmaClass (QueryWithLogin ):
217
303
@@ -228,6 +314,11 @@ def __init__(self):
228
314
self ._sia_url = None
229
315
self ._tap_url = None
230
316
self ._datalink_url = None
317
+ self ._auth = AlmaAuth ()
318
+
319
+ @property
320
+ def auth (self ):
321
+ return self ._auth
231
322
232
323
@property
233
324
def datalink (self ):
@@ -875,11 +966,7 @@ def _get_auth_info(self, username, *, store_password=False,
875
966
else :
876
967
username = self .USERNAME
877
968
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" )
969
+ auth_url = self .auth .host
883
970
884
971
# Get password from keyring or prompt
885
972
password , password_from_keyring = self ._get_password (
@@ -909,69 +996,16 @@ def _login(self, username=None, store_password=False,
909
996
on the keyring. Default is False.
910
997
"""
911
998
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
939
-
999
+ self .auth .set_auth_hosts (auth_urls )
1000
+
940
1001
username , password = self ._get_auth_info (username = username ,
941
1002
store_password = store_password ,
942
1003
reenter_password = reenter_password )
1004
+
1005
+ self .auth .login (username , password )
1006
+ self .USERNAME = username
943
1007
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!" )
973
-
974
- return authenticated
1008
+ return True
975
1009
976
1010
def get_cycle0_uid_contents (self , uid ):
977
1011
"""
0 commit comments