diff --git a/docs/source/hub_conf.rst b/docs/source/hub_conf.rst index 7738fbc8..d8c13bff 100644 --- a/docs/source/hub_conf.rst +++ b/docs/source/hub_conf.rst @@ -112,6 +112,14 @@ General authentication options Whether or not to automatically create a new user from valid ssl or gssapi credentials. + SessionRenewalTimeout + Type: integer + + Default: ``1440`` + + The number of minutes before sessions are required to re-authenticate. + Set to 0 for no timeout. + GSSAPI authentication options ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -553,4 +561,4 @@ We have default checksums types for create rpm checksums. Default: ``md5 sha256`` - Set RPM default checksums type. Default value is set upt to ``md5 sha256``. + Set RPM default checksums type. Default value is set up to ``md5 sha256``. diff --git a/kojihub/app/hub.conf b/kojihub/app/hub.conf index 9ea7aa61..ea4d4c0b 100644 --- a/kojihub/app/hub.conf +++ b/kojihub/app/hub.conf @@ -144,3 +144,6 @@ NotifyOnSuccess = True ## Determines default checksums # RPMDefaultChecksums = md5 sha256 + +# The number of minutes before sessions are required to re-authenticate. Set to 0 for no timeout. +# SessionRenewalTimeout = 1440 diff --git a/kojihub/auth.py b/kojihub/auth.py index 1547fd78..b3e26878 100644 --- a/kojihub/auth.py +++ b/kojihub/auth.py @@ -26,6 +26,7 @@ import random import re import socket import string +import time import six from six.moves import range, urllib @@ -137,7 +138,18 @@ class Session(object): logger.warning("Session ID %s is not related to host IP %s.", self.id, hostip) raise koji.AuthError('Invalid session or bad credentials') - # check for expiration + if not session_data['expired'] and context.opts['SessionRenewalTimeout'] != 0: + renewal_cutoff = (session_data['start_ts'] + + context.opts['SessionRenewalTimeout'] * 60) + if time.time() > renewal_cutoff: + session_data['expired'] = True + update = UpdateProcessor('sessions', + data={'expired': True}, + clauses=['id = %(id)s OR master = %(id)s'], + values={'id': self.id}) + update.execute() + context.cnx.commit() + if session_data['expired']: if getattr(context, 'method') not in AUTH_METHODS: raise koji.AuthExpired('session "%s" has expired' % self.id) diff --git a/kojihub/kojixmlrpc.py b/kojihub/kojixmlrpc.py index 3139071a..e9e0b3d9 100644 --- a/kojihub/kojixmlrpc.py +++ b/kojihub/kojixmlrpc.py @@ -499,7 +499,9 @@ def load_config(environ): ['RegexNameInternal', 'string', r'^[A-Za-z0-9/_.+-]+$'], ['RegexUserName', 'string', r'^[A-Za-z0-9/_.@-]+$'], - ['RPMDefaultChecksums', 'string', 'md5 sha256'] + ['RPMDefaultChecksums', 'string', 'md5 sha256'], + + ['SessionRenewalTimeout', 'integer', 1440], ] opts = {} for name, dtype, default in cfgmap: diff --git a/tests/test_lib/test_auth.py b/tests/test_lib/test_auth.py index 2fae67a8..606101b2 100644 --- a/tests/test_lib/test_auth.py +++ b/tests/test_lib/test_auth.py @@ -49,6 +49,7 @@ class TestAuthSession(unittest.TestCase): self.context.opts = { 'CheckClientIP': True, 'DisableURLSessions': False, + 'SessionRenewalTimeout': 0, } with self.assertRaises(koji.GenericError) as cm: kojihub.auth.Session() @@ -62,6 +63,7 @@ class TestAuthSession(unittest.TestCase): self.context.opts = { 'CheckClientIP': True, 'DisableURLSessions': False, + 'SessionRenewalTimeout': 0, } self.context.environ = { 'QUERY_STRING': 'session-id=123&session-key=xyz&callnum=345', @@ -88,6 +90,7 @@ class TestAuthSession(unittest.TestCase): self.context.opts = { 'CheckClientIP': True, 'DisableURLSessions': True, + 'SessionRenewalTimeout': 0, } self.context.environ = { 'HTTP_KOJI_SESSION_ID': '123', @@ -113,6 +116,58 @@ class TestAuthSession(unittest.TestCase): def test_session_old(self): self.get_session_old() + def test_renewal_timeout(self): + """Simple kojihub.auth.Session instance""" + self.context.opts = { + 'CheckClientIP': True, + 'DisableURLSessions': False, + 'SessionRenewalTimeout': 1440, + } + self.context.environ = { + 'QUERY_STRING': 'session-id=123&session-key=xyz&callnum=345', + 'REMOTE_ADDR': 'remote-addr', + } + + self.query_executeOne.side_effect = [ + {'authtype': 2, 'callnum': 1, "start_ts": 1666599426.227002, + "update_ts": 1666599426.254308, 'exclusive': None, + 'expired': False, 'master': None, + 'start_time': datetime.datetime(2022, 10, 24, 8, 17, 6, 227002, + tzinfo=datetime.timezone.utc), + 'update_time': datetime.datetime(2022, 10, 24, 8, 17, 6, 254308, + tzinfo=datetime.timezone.utc), + 'user_id': 1}, + {'name': 'kojiadmin', 'status': 0, 'usertype': 0}] + with self.assertRaises(koji.GenericError) as cm: + kojihub.auth.Session() + # no args in request/environment + self.assertEqual(cm.exception.args[0], 'session "123" has expired') + + self.assertEqual(len(self.updates), 1) + self.assertEqual(len(self.queries), 1) + + update = self.updates[0] + + self.assertEqual(update.table, 'sessions') + self.assertEqual(update.values['id'], 123) + self.assertEqual(update.clauses, ['id = %(id)s OR master = %(id)s']) + self.assertEqual(update.data, {'expired': True}) + self.assertEqual(update.rawdata, {}) + + query = self.queries[0] + self.assertEqual(query.tables, ['sessions']) + self.assertEqual(query.joins, None) + self.assertEqual(query.clauses, ['closed IS FALSE', 'hostip = %(hostip)s', 'id = %(id)i', + 'key = %(key)s']) + self.assertEqual(query.columns, ['authtype', 'callnum', 'exclusive', 'expired', 'master', + 'start_time', "date_part('epoch', start_time)", + 'update_time', "date_part('epoch', update_time)", + 'user_id']) + self.assertEqual(query.aliases, ['authtype', 'callnum', 'exclusive', 'expired', 'master', + 'start_time', 'start_ts', 'update_time', 'update_ts', + 'user_id']) + self.assertEqual(query.values, {'id': 123, 'key': 'xyz', 'hostip': 'remote-addr'}) + def test_basic_instance(self): """auth.Session instance""" s, cntext = self.get_session()