PR#3760: Add renewal session timeout

Merges #3760
https://pagure.io/koji/pull-request/3760

Fixes: #3596
https://pagure.io/koji/issue/3596
Renewal timeout option
This commit is contained in:
Tomas Kopecek 2023-05-09 09:16:48 +02:00
commit 747f17824a
8 changed files with 110 additions and 13 deletions

View file

@ -0,0 +1,7 @@
-- upgrade script to migrate the Koji database schema
-- from version 1.32 to 1.33
BEGIN;
ALTER TABLE sessions ADD COLUMN renew_time TIMESTAMPTZ;
COMMIT;

View file

@ -120,6 +120,7 @@ CREATE TABLE sessions (
update_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
exclusive BOOLEAN CHECK (exclusive),
closed BOOLEAN NOT NULL DEFAULT FALSE,
renew_time TIMESTAMPTZ,
CONSTRAINT no_exclusive_subsessions CHECK (
master IS NULL OR "exclusive" IS NULL),
CONSTRAINT no_closed_exclusive CHECK (

View file

@ -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``.

View file

@ -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

View file

@ -26,6 +26,7 @@ import random
import re
import socket
import string
import time
import six
from six.moves import range, urllib
@ -116,7 +117,8 @@ class Session(object):
fields = (('authtype', 'authtype'), ('callnum', 'callnum'), ('exclusive', 'exclusive'),
('expired', 'expired'), ('master', 'master'), ('start_time', 'start_time'),
('update_time', 'update_time'), ("date_part('epoch', start_time)", 'start_ts'),
("date_part('epoch', update_time)", 'update_ts'), ('user_id', 'user_id'))
("date_part('epoch', update_time)", 'update_ts'), ('user_id', 'user_id'),
('renew_time', 'renew_time'), ("date_part('epoch', renew_time)", 'renew_ts'))
columns, aliases = zip(*fields)
query = QueryProcessor(tables=['sessions'], columns=columns, aliases=aliases,
@ -137,7 +139,22 @@ 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:
if session_data['renew_ts']:
renewal_cutoff = (session_data['renew_ts'] +
context.opts['SessionRenewalTimeout'] * 60)
else:
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)
@ -523,7 +540,7 @@ class Session(object):
update = UpdateProcessor('sessions',
clauses=['id=%(id)i'],
rawdata={'update_time': 'NOW()'},
rawdata={'update_time': 'NOW()', 'renew_time': 'NOW()'},
data={'key': self.key, 'expired': False},
values={'id': self.id})
update.execute()

View file

@ -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:

View file

@ -41,8 +41,8 @@ class TestGetSessionInfo(DBQueryTestCase):
self.assertEqual(query.clauses, ['expired is FALSE', 'user_id = %(user_id)i'])
self.assertEqual(query.joins, None)
self.assertEqual(query.columns, ['authtype', 'callnum', 'exclusive', 'expired', 'master',
"date_part('epoch', start_time)", 'update_time',
'user_id'])
"date_part('epoch', start_time)",
'update_time', 'user_id'])
self.assertEqual(query.aliases, ['authtype', 'callnum', 'exclusive', 'expired', 'master',
'start_time', 'update_time', 'user_id'])
@ -71,8 +71,8 @@ class TestGetSessionInfo(DBQueryTestCase):
self.assertEqual(query.clauses, ['expired is FALSE', 'user_id = %(user_id)i'])
self.assertEqual(query.joins, None)
self.assertEqual(query.columns, ['authtype', 'callnum', 'exclusive', 'expired', 'master',
"date_part('epoch', start_time)", 'update_time',
'user_id'])
"date_part('epoch', start_time)",
'update_time', 'user_id'])
self.assertEqual(query.aliases, ['authtype', 'callnum', 'exclusive', 'expired', 'master',
'start_time', 'update_time', 'user_id'])

View file

@ -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,60 @@ 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),
'renew_ts': None,
'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',
'renew_time', "date_part('epoch', renew_time)",
'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',
'renew_time', 'renew_ts', '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()
@ -141,12 +198,13 @@ class TestAuthSession(unittest.TestCase):
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',
'renew_time', "date_part('epoch', renew_time)",
'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'])
'renew_time', 'renew_ts', 'start_time', 'start_ts',
'update_time', 'update_ts', 'user_id'])
self.assertEqual(query.values, {'id': 123, 'key': 'xyz', 'hostip': 'remote-addr'})
query = self.queries[1]
@ -192,12 +250,13 @@ class TestAuthSession(unittest.TestCase):
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',
'renew_time', "date_part('epoch', renew_time)",
'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'])
'renew_time', 'renew_ts', 'start_time', 'start_ts',
'update_time', 'update_ts', 'user_id'])
self.assertEqual(query.values, {'id': 123, 'key': 'xyz', 'hostip': 'remote-addr'})
query = self.queries[1]