1+ import importlib
2+ import logging
3+ import weakref
4+ from irods .api_number import api_number
5+ from irods .message import iRODSMessage , JSON_Message
6+ import irods .password_obfuscation as obf
7+ import irods .session
8+
9+
110__all__ = ["pam_password" , "native" ]
211
12+
313AUTH_PLUGIN_PACKAGE = "irods.auth"
414
5- import importlib
15+
16+ # Python3 does not have types.NoneType
17+ _NoneType = type (None )
18+
19+
20+ class AuthStorage :
21+ """A class that facilitates flexible means password storage.
22+
23+ Using an instance of this class, passwords may either be
24+
25+ - directly placed in a member attribute (pw), or
26+
27+ - they may be written to / read from a specified file path in encoded
28+ form, usually in an .irodsA file intended for iRODS client authentication.
29+
30+ Most typical of this class's utility is the transfer of password information from
31+ the pam_password to the native authentication flow. In this usage, whether the
32+ password is stored in RAM or in the filesystem depends on whether it was read
33+ originally as a function parameter or from an authentication file, respectively,
34+ when the session was created.
35+
36+ """
37+
38+ @staticmethod
39+ def get_env_password (filename = None ):
40+ options = dict (irods_authentication_file = filename ) if filename else {}
41+ return irods .session .iRODSSession .get_irods_password (** options )
42+
43+ @staticmethod
44+ def get_env_password_file ():
45+ return irods .session .iRODSSession .get_irods_password_file ()
46+
47+ @staticmethod
48+ def set_env_password (unencoded_pw , filename = None ):
49+ if filename is None :
50+ filename = AuthStorage .get_env_password_file ()
51+ from ..client_init import _open_file_for_protected_contents
52+
53+ with _open_file_for_protected_contents (filename , "w" ) as irodsA :
54+ irodsA .write (obf .encode (unencoded_pw ))
55+ return filename
56+
57+ @staticmethod
58+ def get_temp_pw_storage (conn ):
59+ """Fetch the AuthStorage instance associated with this connection object."""
60+ return getattr (conn , "auth_storage" , lambda : None )()
61+
62+ @staticmethod
63+ def create_temp_pw_storage (conn ):
64+ """Creates an AuthStorage instance to be cached and associated with this connection object.
65+
66+ Called multiple times for the same connection, it will return the cached instance.
67+
68+ The value returned by this call should be stored by the caller into an appropriately scoped
69+ variable to ensure the AuthStorage instance endures for the desired lifetime -- that is,
70+ for however long we wish to keep the password information around. This is because the
71+ connection object only maintains a weak reference to said instance.
72+ """
73+
74+ # resolve the weakly referenced AuthStorage obj for the connection if there is one.
75+ weakref_to_store = getattr (conn , "auth_storage" , None )
76+ store = weakref_to_store and weakref_to_store ()
77+
78+ # In absence of a persistent AuthStorage object, create one.
79+ if store is None :
80+ store = AuthStorage (conn )
81+ # So that the connection object doesn't hold on to password data too long:
82+ conn .auth_storage = weakref .ref (store )
83+ return store
84+
85+ def __init__ (self , conn ):
86+ self .conn = conn
87+ self .pw = ""
88+ self ._auth_file = ""
89+
90+ @property
91+ def auth_file (self ):
92+ if self ._auth_file is None :
93+ return ""
94+ return self ._auth_file or self .conn .account .derived_auth_file
95+
96+ def use_client_auth_file (self , auth_file ):
97+ """Set to None to completely suppress use of an .irodsA auth file."""
98+ if isinstance (auth_file , (str , _NoneType )):
99+ self ._auth_file = auth_file
100+ else :
101+ msg = f"Invalid object in { self .__class__ } ._auth_file"
102+ raise RuntimeError (msg )
103+
104+ def store_pw (self , pw ):
105+ if self .auth_file :
106+ self .set_env_password (pw , filename = self .auth_file )
107+ else :
108+ self .pw = pw
109+
110+ def retrieve_pw (self ):
111+ if self .auth_file :
112+ return self .get_env_password (filename = self .auth_file )
113+ return self .pw
6114
7115
8116def load_plugins (subset = set (), _reload = False ):
@@ -18,9 +126,74 @@ def load_plugins(subset=set(), _reload=False):
18126 return dir_
19127
20128
21- # TODO(#499): X models a class which we could define here as a base for various server or client state machines
22- # as appropriate for the various authentication types.
129+ class REQUEST_IS_MISSING_KEY ( Exception ):
130+ pass
23131
24132
25- class X :
26- pass
133+ def throw_if_request_message_is_missing_key (request , required_keys ):
134+ for key in required_keys :
135+ if not key in request :
136+ raise REQUEST_IS_MISSING_KEY (f"key = { key } " )
137+
138+
139+ def _auth_api_request (conn , data ):
140+ message_body = JSON_Message (data , conn .server_version )
141+ message = iRODSMessage (
142+ "RODS_API_REQ" , msg = message_body , int_info = api_number ["AUTHENTICATION_APN" ]
143+ )
144+ conn .send (message )
145+ response = conn .recv ()
146+ return response .get_json_encoded_struct ()
147+
148+
149+ __FLOW_COMPLETE__ = "authentication_flow_complete"
150+ __NEXT_OPERATION__ = "next_operation"
151+
152+
153+ CLIENT_GET_REQUEST_RESULT = "client_get_request_result"
154+ FORCE_PASSWORD_PROMPT = "force_password_prompt"
155+ STORE_PASSWORD_IN_MEMORY = "store_password_in_memory"
156+
157+
158+ class authentication_base :
159+
160+ def __init__ (self , connection , scheme ):
161+ self .conn = connection
162+ self .loggedIn = 0
163+ self .scheme = scheme
164+
165+ def call (self , next_operation , request ):
166+ logging .info ("next operation = %r" , next_operation )
167+ old_func = func = next_operation
168+ # One level of indirection should be sufficient to get a callable method.
169+ if not callable (func ):
170+ old_func , func = (func , getattr (self , func , None ))
171+ func = func or old_func
172+ if not callable (func ):
173+ raise RuntimeError ("client request contains no callable 'next_operation'" )
174+ resp = func (request )
175+ logging .info ("resp = %r" , resp )
176+ return resp
177+
178+ def authenticate_client (
179+ self , next_operation = "auth_client_start" , initial_request = {}
180+ ):
181+ to_send = initial_request .copy ()
182+ to_send ["scheme" ] = self .scheme
183+
184+ while True :
185+ resp = self .call (next_operation , to_send )
186+ if self .loggedIn :
187+ break
188+ next_operation = resp .get (__NEXT_OPERATION__ )
189+ if next_operation is None :
190+ raise ClientAuthError (
191+ "next_operation key missing; cannot determine next operation"
192+ )
193+ if next_operation in (__FLOW_COMPLETE__ , "" ):
194+ raise ClientAuthError (
195+ f"authentication flow stopped without success: scheme = { self .scheme } "
196+ )
197+ to_send = resp
198+
199+ logging .info ("fully authenticated" )
0 commit comments