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
+
1
10
__all__ = ["pam_password" , "native" ]
2
11
12
+
3
13
AUTH_PLUGIN_PACKAGE = "irods.auth"
4
14
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 one of the following:
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
+ @staticmethod
38
+ def get_env_password (filename = None ):
39
+ options = dict (irods_authentication_file = filename ) if filename else {}
40
+ return irods .session .iRODSSession .get_irods_password (** options )
41
+
42
+ @staticmethod
43
+ def get_env_password_file ():
44
+ return irods .session .iRODSSession .get_irods_password_file ()
45
+
46
+ @staticmethod
47
+ def set_env_password (unencoded_pw , filename = None ):
48
+ if filename is None :
49
+ filename = AuthStorage .get_env_password_file ()
50
+ from ..client_init import _open_file_for_protected_contents
51
+
52
+ with _open_file_for_protected_contents (filename , "w" ) as irodsA :
53
+ irodsA .write (obf .encode (unencoded_pw ))
54
+ return filename
55
+
56
+ @staticmethod
57
+ def get_temp_pw_storage (conn ):
58
+ """Fetch the AuthStorage instance associated with this connection object."""
59
+ return getattr (conn , "auth_storage" , lambda : None )()
60
+
61
+ @staticmethod
62
+ def create_temp_pw_storage (conn ):
63
+ """Creates an AuthStorage instance to be cached and associated with this connection object.
64
+
65
+ Called multiple times for the same connection, it will return the cached instance.
66
+
67
+ The value returned by this call should be stored by the caller into an appropriately scoped
68
+ variable to ensure the AuthStorage instance endures for the desired lifetime -- that is,
69
+ for however long we wish to keep the password information around. This is because the
70
+ connection object only maintains a weak reference to said instance.
71
+ """
72
+
73
+ # resolve the weakly referenced AuthStorage obj for the connection if there is one.
74
+ weakref_to_store = getattr (conn , "auth_storage" , None )
75
+ store = weakref_to_store and weakref_to_store ()
76
+
77
+ # In absence of a persistent AuthStorage object, create one.
78
+ if store is None :
79
+ store = AuthStorage (conn )
80
+ # So that the connection object doesn't hold on to password data too long:
81
+ conn .auth_storage = weakref .ref (store )
82
+ return store
83
+
84
+ def __init__ (self , conn ):
85
+ self .conn = conn
86
+ self .pw = ""
87
+ self ._auth_file = ""
88
+
89
+ @property
90
+ def auth_file (self ):
91
+ if self ._auth_file is None :
92
+ return ""
93
+ return self ._auth_file or self .conn .account .derived_auth_file
94
+
95
+ def use_client_auth_file (self , auth_file ):
96
+ """Set to None to completely suppress use of an .irodsA auth file."""
97
+ if isinstance (auth_file , (str , _NoneType )):
98
+ self ._auth_file = auth_file
99
+ else :
100
+ msg = f"Invalid object in { self .__class__ } ._auth_file"
101
+ raise RuntimeError (msg )
102
+
103
+ def store_pw (self , pw ):
104
+ if self .auth_file :
105
+ self .set_env_password (pw , filename = self .auth_file )
106
+ else :
107
+ self .pw = pw
108
+
109
+ def retrieve_pw (self ):
110
+ if self .auth_file :
111
+ return self .get_env_password (filename = self .auth_file )
112
+ return self .pw
6
113
7
114
8
115
def load_plugins (subset = set (), _reload = False ):
@@ -18,9 +125,81 @@ def load_plugins(subset=set(), _reload=False):
18
125
return dir_
19
126
20
127
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.
128
+ class REQUEST_IS_MISSING_KEY ( Exception ):
129
+ pass
23
130
24
131
25
- class X :
132
+ class ClientAuthError ( Exception ) :
26
133
pass
134
+
135
+
136
+ def throw_if_request_message_is_missing_key (request , required_keys ):
137
+ for key in required_keys :
138
+ if not key in request :
139
+ raise REQUEST_IS_MISSING_KEY (f"key = { key } " )
140
+
141
+
142
+ def _auth_api_request (conn , data ):
143
+ message_body = JSON_Message (data , conn .server_version )
144
+ message = iRODSMessage (
145
+ "RODS_API_REQ" , msg = message_body , int_info = api_number ["AUTHENTICATION_APN" ]
146
+ )
147
+ conn .send (message )
148
+ response = conn .recv ()
149
+ return response .get_json_encoded_struct ()
150
+
151
+
152
+ __FLOW_COMPLETE__ = "authentication_flow_complete"
153
+ __NEXT_OPERATION__ = "next_operation"
154
+
155
+
156
+ CLIENT_GET_REQUEST_RESULT = "client_get_request_result"
157
+ FORCE_PASSWORD_PROMPT = "force_password_prompt"
158
+ STORE_PASSWORD_IN_MEMORY = "store_password_in_memory"
159
+
160
+
161
+ class authentication_base :
162
+
163
+ def __init__ (self , connection , scheme ):
164
+ self .conn = connection
165
+ self .loggedIn = 0
166
+ self .scheme = scheme
167
+
168
+ def call (self , next_operation , request ):
169
+ logging .info ("next operation = %r" , next_operation )
170
+ old_func = func = next_operation
171
+ # One level of indirection should be sufficient to get a callable method.
172
+ if not callable (func ):
173
+ old_func , func = (func , getattr (self , func , None ))
174
+ func = func or old_func
175
+ if not callable (func ):
176
+ raise RuntimeError ("client request contains no callable 'next_operation'" )
177
+ resp = func (request )
178
+ logging .info ("resp = %r" , resp )
179
+ return resp
180
+
181
+ def authenticate_client (
182
+ self , next_operation = "auth_client_start" , initial_request = ()
183
+ ):
184
+ if not isinstance (initial_request , dict ):
185
+ initial_request = dict (initial_request )
186
+
187
+ to_send = initial_request .copy ()
188
+ to_send ["scheme" ] = self .scheme
189
+
190
+ while True :
191
+ resp = self .call (next_operation , to_send )
192
+ if self .loggedIn :
193
+ break
194
+ next_operation = resp .get (__NEXT_OPERATION__ )
195
+ if next_operation is None :
196
+ raise ClientAuthError (
197
+ "next_operation key missing; cannot determine next operation"
198
+ )
199
+ if next_operation in (__FLOW_COMPLETE__ , "" ):
200
+ raise ClientAuthError (
201
+ f"authentication flow stopped without success: scheme = { self .scheme } "
202
+ )
203
+ to_send = resp
204
+
205
+ logging .info ("fully authenticated" )
0 commit comments