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:
commit
747f17824a
8 changed files with 110 additions and 13 deletions
7
docs/schema-upgrade-1.32-1.33.sql
Normal file
7
docs/schema-upgrade-1.32-1.33.sql
Normal 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;
|
||||
|
||||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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``.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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'])
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue