From 0e07429d6659978217d1006ceb5911508b57ee11 Mon Sep 17 00:00:00 2001 From: Jana Cupova Date: Tue, 13 Sep 2022 12:44:49 +0200 Subject: [PATCH] Create new session when old session was timeout Fixes: https://pagure.io/koji/issue/3394 --- koji/__init__.py | 61 +++++++++++++++++++++++++++++++---- koji/auth.py | 21 +++++++++--- tests/test_lib/test_gssapi.py | 6 ++-- 3 files changed, 74 insertions(+), 14 deletions(-) diff --git a/koji/__init__.py b/koji/__init__.py index 50162f8e..883550ae 100644 --- a/koji/__init__.py +++ b/koji/__init__.py @@ -2408,6 +2408,9 @@ def grab_session_options(options): 'upload_blocksize', 'no_ssl_verify', 'serverca', + 'keytab', + 'principal', + 'ccache', ) # cert is omitted for now if isinstance(options, dict): @@ -2444,6 +2447,8 @@ class ClientSession(object): self.rsession = None self.new_session() self.opts.setdefault('timeout', DEFAULT_REQUEST_TIMEOUT) + self.exclusive = False + self.hostip = None @property def multicall(self): @@ -2475,13 +2480,16 @@ class ClientSession(object): self.callnum = None # do we need to do anything else here? self.authtype = None + self.session_key = None else: self.logged_in = True self.callnum = 0 + self.session_key = sinfo['session-key'] self.sinfo = sinfo def login(self, opts=None): - sinfo = self.callMethod('login', self.opts['user'], self.opts['password'], opts) + sinfo = self.callMethod('login', self.opts['user'], self.opts['password'], + self.opts['session_key'], opts) if not sinfo: return False self.setSession(sinfo) @@ -2494,7 +2502,7 @@ class ClientSession(object): return type(self)(self.baseurl, self.opts, sinfo) def gssapi_login(self, principal=None, keytab=None, ccache=None, - proxyuser=None, proxyauthtype=None): + proxyuser=None, proxyauthtype=None, session_key=None): if not reqgssapi: raise PythonImportError( "Please install python-requests-gssapi to use GSSAPI." @@ -2539,7 +2547,7 @@ class ClientSession(object): # will fail with a handshake failure, which is retried by default. # For this case we're now using retry=False and test errors for # this exact usecase. - kwargs = {'proxyuser': proxyuser} + kwargs = {'proxyuser': proxyuser, 'session_key': session_key} if proxyauthtype is not None: kwargs['proxyauthtype'] = proxyauthtype for tries in range(self.opts.get('max_retries', 30)): @@ -2587,7 +2595,8 @@ class ClientSession(object): self.authtype = AUTHTYPES['GSSAPI'] return True - def ssl_login(self, cert=None, ca=None, serverca=None, proxyuser=None, proxyauthtype=None): + def ssl_login(self, cert=None, ca=None, serverca=None, proxyuser=None, proxyauthtype=None, + session_key=None): cert = cert or self.opts.get('cert') serverca = serverca or self.opts.get('serverca') if cert is None: @@ -2616,7 +2625,7 @@ class ClientSession(object): self.opts['serverca'] = serverca e_str = None try: - kwargs = {'proxyuser': proxyuser} + kwargs = {'proxyuser': proxyuser, 'session_key': session_key} if proxyauthtype is not None: kwargs['proxyauthtype'] = proxyauthtype sinfo = self._callMethod('sslLogin', [], kwargs) @@ -2833,6 +2842,32 @@ class ClientSession(object): result = result[0] return result + def _renew_session(self): + session_key = self.session_key + self.setSession(None) + if self.authtype == 'SSL' or \ + (self.opts.get('cert') and os.path.isfile(self.opts['cert'])): + self.ssl_login(cert=self.opts['cert'], + serverca=self.opts['serverca'], + session_key=session_key) + elif self.authtype == 'NORMAL' or self.opts.get('user'): + self.login(user=self.opts['user'], password=self.opts['password'], + session_key=session_key) + elif self.authtype in ['KERBEROS', 'GSSAPI'] or \ + self.opts.get('krb_principal'): + authtype = self.authtype or AUTHTYPES['GSSAPI'] + principal = self.opts.get('principal') + keytab = self.opts.get('keytab') + ccache = self.opts.get('ccache') + if authtype == 'KERBEROS': + self.krb_login(principal=principal, keytab=keytab, + ccache=ccache, session_key=session_key) + elif authtype == 'GSSAPI': + self.gssapi_login(self, principal=principal, keytab=keytab, + ccache=ccache, session_key=session_key) + if self.exclusive: + self.exclusiveSession() + def _callMethod(self, name, args, kwargs=None, retry=True): """Make a call to the hub with retries and other niceties""" @@ -2871,7 +2906,16 @@ class ClientSession(object): # server correctly reporting an outage tries = 0 continue - raise err + elif isinstance(err, AuthExpired): + if self.logged_in: + self._renew_session() + return self._callMethod(name, args, kwargs, retry) + else: + raise AuthError("Session ID %s is unlogged and expired." % + self.sinfo['session-id']) + else: + raise err + except (SystemExit, KeyboardInterrupt): # (depending on the python version, these may or may not be subclasses of # Exception) @@ -3183,6 +3227,11 @@ class ClientSession(object): result = self.callMethod('downloadTaskOutput', taskID, fileName, **dlopts) return base64.b64decode(result) + def exclusiveSession(self, force=False): + """Make this session exclusive""" + self._callMethod('exclusiveSession', {'force': force}) + self.exclusive = True + class MultiCallHack(object): """Workaround of a terribly overloaded namespace diff --git a/koji/auth.py b/koji/auth.py index 74b41a3e..2da0e377 100644 --- a/koji/auth.py +++ b/koji/auth.py @@ -280,7 +280,7 @@ class Session(object): if result['status'] != koji.USER_STATUS['NORMAL']: raise koji.AuthError('logins by %s are not allowed' % result['name']) - def login(self, user, password, opts=None): + def login(self, user, password, session_key=None, opts=None): """create a login session""" if opts is None: opts = {} @@ -301,7 +301,8 @@ class Session(object): self.checkLoginAllowed(user_id) # create session and return - sinfo = self.createSession(user_id, hostip, koji.AUTHTYPES['NORMAL']) + sinfo = self.createSession(user_id, hostip, koji.AUTHTYPES['NORMAL'], + session_key=session_key) context.cnx.commit() return sinfo @@ -326,7 +327,7 @@ class Session(object): return (local_ip, local_port, remote_ip, remote_port) - def sslLogin(self, proxyuser=None, proxyauthtype=None): + def sslLogin(self, proxyuser=None, proxyauthtype=None, session_key=None): """Login into brew via SSL. proxyuser name can be specified and if it is allowed in the configuration file then connection is allowed to login as @@ -404,7 +405,7 @@ class Session(object): hostip = self.get_remote_ip() - sinfo = self.createSession(user_id, hostip, authtype) + sinfo = self.createSession(user_id, hostip, authtype, session_key=session_key) return sinfo def makeExclusive(self, force=False): @@ -483,12 +484,22 @@ class Session(object): update.execute() context.cnx.commit() - def createSession(self, user_id, hostip, authtype, master=None): + def createSession(self, user_id, hostip, authtype, master=None, session_key=None): """Create a new session for the given user. Return a map containing the session-id and session-key. If master is specified, create a subsession """ + if session_key: + query = QueryProcessor(tables=['sessions'], columns=['master'], + clauses=['key=%(session_key)d'], + values={'session_key': session_key}) + row = query.executeOne(strict=False) + if not row: + raise koji.GenericError("Don't allow to renew subsession, " + "subsession doesn't exist.") + master = row['master'] + # generate a random key alnum = string.ascii_letters + string.digits key = "%s-%s" % (user_id, diff --git a/tests/test_lib/test_gssapi.py b/tests/test_lib/test_gssapi.py index 6249221c..1c7d9188 100644 --- a/tests/test_lib/test_gssapi.py +++ b/tests/test_lib/test_gssapi.py @@ -27,7 +27,7 @@ class TestGSSAPI(unittest.TestCase): old_environ = dict(**os.environ) self.session.gssapi_login() self.session._callMethod.assert_called_with( - 'sslLogin', [], {'proxyuser': None}, retry=False) + 'sslLogin', [], {'proxyuser': None, 'session_key': None}, retry=False) self.assertEqual(old_environ, dict(**os.environ)) @mock.patch('koji.reqgssapi.HTTPKerberosAuth') @@ -47,7 +47,7 @@ class TestGSSAPI(unittest.TestCase): koji.reqgssapi.__version__ = accepted_version rv = self.session.gssapi_login(principal, keytab, ccache) self.session._callMethod.assert_called_with( - 'sslLogin', [], {'proxyuser': None}, retry=False) + 'sslLogin', [], {'proxyuser': None, 'session_key': None}, retry=False) self.assertEqual(old_environ, dict(**os.environ)) self.assertTrue(rv) self.session._callMethod.reset_mock() @@ -84,7 +84,7 @@ class TestGSSAPI(unittest.TestCase): with self.assertRaises(koji.GSSAPIAuthError): self.session.gssapi_login() self.session._callMethod.assert_called_with( - 'sslLogin', [], {'proxyuser': None}, retry=False) + 'sslLogin', [], {'proxyuser': None, 'session_key': None}, retry=False) self.assertEqual(old_environ, dict(**os.environ)) def test_gssapi_login_http(self):