11/*
22 * migauthhandler.c - C <-> Python wrappers for MiG user authentication
3- * Copyright (C) 2003-2023 The MiG Project lead by Brian Vinter
3+ * Copyright (C) 2003-2025 The MiG Project lead by the Science HPC Center at UCPH
44 *
55 * This file is part of MiG
66 *
8484#define RATE_LIMIT_EXPIRE_DELAY 120
8585#endif
8686
87+ #ifndef MAX_AUTH_TRIES
88+ #define MAX_AUTH_TRIES 3
89+ #endif
90+
8791void * libpython_handle = NULL ;
92+ unsigned int migauth_tries = 0 ;
93+ bool migauth_exit = false;
94+
8895PyObject * py_main = NULL ;
8996
9097static void pyrun (const char * cmd , ...)
@@ -110,16 +117,19 @@ static bool mig_pyinit()
110117{
111118 // https://stackoverflow.com/questions/11842920/undefined-symbol-pyexc-importerror-when-embedding-python-in-c/50489814#50489814
112119 if (libpython_handle != NULL ) {
113- WRITELOGMESSAGE (LOG_DEBUG , "Python already initialized\n" );
120+ migauth_tries += 1 ;
121+ WRITELOGMESSAGE (LOG_DEBUG ,
122+ "Python already initialized with migauth_tries: %d/%d\n" ,
123+ migauth_tries , MAX_AUTH_TRIES );
114124 } else {
125+ migauth_tries = 1 ;
115126 // NOTE: use make-detected LIBPYTHON shared library and RTLD_NOW
116127 // NOTE: The issue with RTLD_LAZY is that C-extensions do not have dependency on the libpython
117128 // (as can be seen with help of ldd), so once they are loaded and a symbol (e.g. PyFloat_Type)
118129 // from libpython which is not yet resolved must be looked up,
119130 // the dynamic linker doesn't know that it has to look into the libpython.
120131 // https://stackoverflow.com/questions/67891197/ctypes-cpython-39-x86-64-linux-gnu-so-undefined-symbol-pyfloat-type-in-embedd
121132 libpython_handle = dlopen (LIBPYTHON , RTLD_NOW | RTLD_GLOBAL );
122-
123133 #if PY_VERSION_HEX < 0x03000000
124134 Py_SetProgramName ("pam-mig" );
125135 #else
@@ -136,6 +146,9 @@ static bool mig_pyinit()
136146 WRITELOGMESSAGE (LOG_ERR , "Failed to find Python __main__\n" );
137147 return false;
138148 }
149+ WRITELOGMESSAGE (LOG_DEBUG ,
150+ "Python initialized with migauth_tries: %d/%d\n" ,
151+ migauth_tries , MAX_AUTH_TRIES );
139152 pyrun ("from __future__ import absolute_import" );
140153
141154 pyrun ("import os" );
@@ -164,14 +177,24 @@ static bool mig_pyinit()
164177 return true;
165178}
166179
167- static bool mig_pyexit ()
180+ static bool mig_pyexit (int exit_value )
168181{
169182 if (libpython_handle == NULL ) {
170183 WRITELOGMESSAGE (LOG_DEBUG , "Python already finalized\n" );
171- } else {
184+ } else if (exit_value == PAM_SUCCESS \
185+ || migauth_exit == true \
186+ || migauth_tries >= MAX_AUTH_TRIES ) {
187+ WRITELOGMESSAGE (LOG_DEBUG ,
188+ "Python finalize with exit value: %d, migauth_exit: %d, migauth_tries: %d/%d\n" ,
189+ exit_value , migauth_exit , migauth_tries , MAX_AUTH_TRIES );
172190 Py_Finalize ();
173191 dlclose (libpython_handle );
174192 libpython_handle = NULL ;
193+ migauth_exit = true;
194+ } else {
195+ WRITELOGMESSAGE (LOG_DEBUG ,
196+ "mig_pyexit called with exit_value: %d migauth_tries: %d/%d\n" ,
197+ exit_value , migauth_tries , MAX_AUTH_TRIES );
175198 }
176199 return true;
177200}
@@ -331,20 +354,26 @@ static bool mig_reg_auth_attempt(const unsigned int mode,
331354 const char * address , const char * secret )
332355{
333356 bool result = false;
357+ bool disconnect = true;
334358 WRITELOGMESSAGE (LOG_DEBUG ,
335359 "mode: 0x%X, username: %s, address: %s, secret: %s\n" ,
336360 mode , username , address , secret );
337- char pycmd [MAX_PYCMD_LENGTH ] =
338- "(authorized, disconnect) = validate_auth_attempt(configuration, 'sftp-subsys', " ;
339- char pytmp [MAX_PYCMD_LENGTH ];
361+ /* Filter valid auth types first - we currently only allow password auth */
340362 if (mode & MIG_AUTHTYPE_PASSWORD ) {
341- strncat ( & pycmd [ 0 ] , "'password', " , MAX_PYCMD_LENGTH - strlen ( pycmd ) );
363+ WRITELOGMESSAGE ( LOG_DEBUG , "proceed with password authentication\n" );
342364 } else {
343365 WRITELOGMESSAGE (LOG_ERR ,
344366 "mig_reg_auth_attempt: No valid auth-type in mode: 0x%X\n" ,
345367 mode );
368+ /* We don't exit hard here to make sure other auth types may follow */
346369 return false;
347370 }
371+ char pycmd [MAX_PYCMD_LENGTH ] =
372+ "(authorized, disconnect) = validate_auth_attempt(configuration, 'sftp-subsys', " ;
373+ char pytmp [MAX_PYCMD_LENGTH ];
374+ /* Always password auth here as mentioned in the above comment */
375+ strncat (& pycmd [0 ], "'password', " , MAX_PYCMD_LENGTH - strlen (pycmd ));
376+
348377 strncat (& pycmd [0 ], "'" , MAX_PYCMD_LENGTH - strlen (pycmd ));
349378 strncat (& pycmd [0 ], username , MAX_PYCMD_LENGTH - strlen (pycmd ));
350379 strncat (& pycmd [0 ], "', '" , MAX_PYCMD_LENGTH - strlen (pycmd ));
@@ -415,17 +444,36 @@ static bool mig_reg_auth_attempt(const unsigned int mode,
415444 MAX_PYCMD_LENGTH - strlen (pycmd ));
416445 }
417446 strncat (& pycmd [0 ], ")" , MAX_PYCMD_LENGTH - strlen (pycmd ));
418- if (MAX_PYCMD_LENGTH == strlen (pycmd )) {
419- WRITELOGMESSAGE (LOG_ERR , "mig_reg_auth_attempt: pycmd overflow\n" );
420- return false;
421- }
422- pyrun (& pycmd [0 ]);
423- PyObject * py_authorized = PyObject_GetAttrString (py_main , "authorized" );
424- if (py_authorized == NULL ) {
425- WRITELOGMESSAGE (LOG_ERR , "Missing python variable: py_authorized\n" );
447+ /* Execute python command if and only if it didn't overflow */
448+ if (MAX_PYCMD_LENGTH > strlen (pycmd )) {
449+ pyrun (& pycmd [0 ]);
450+ PyObject * py_authorized = PyObject_GetAttrString (py_main , "authorized" );
451+ if (py_authorized == NULL ) {
452+ WRITELOGMESSAGE (LOG_ERR , "Missing python variable: py_authorized\n" );
453+ } else {
454+ result = PyObject_IsTrue (py_authorized );
455+ Py_DECREF (py_authorized );
456+ }
457+ PyObject * py_disconnect = PyObject_GetAttrString (py_main , "disconnect" );
458+ if (py_disconnect == NULL ) {
459+ WRITELOGMESSAGE (LOG_ERR , "Missing python variable: py_disconnect\n" );
460+ } else {
461+ disconnect = PyObject_IsTrue (py_disconnect );
462+ Py_DECREF (py_disconnect );
463+ }
426464 } else {
427- result = PyObject_IsTrue (py_authorized );
428- Py_DECREF (py_authorized );
465+ WRITELOGMESSAGE (LOG_ERR , "mig_reg_auth_attempt: pycmd overflow!\n" );
466+ }
467+
468+ /* NOTE: '(mode & MIG_VALID_AUTH)'
469+ If caller (libpam_mig) validated credentials
470+ then there are no more passwords (re-)tries.
471+ We honor 'disconnect' here to follow central password policy.
472+ */
473+ if (disconnect == true || (mode & MIG_VALID_AUTH )) {
474+ WRITELOGMESSAGE (LOG_DEBUG , "disconnect: %d, mode & MIG_VALID_AUTH: %d\n" ,
475+ disconnect , (mode & MIG_VALID_AUTH ));
476+ migauth_exit = true;
429477 }
430478
431479 return result ;
0 commit comments