parent
b04ce33fa0
commit
7897628159
9 changed files with 265 additions and 12 deletions
|
|
@ -10257,11 +10257,54 @@ class RootExports(object):
|
|||
"""Return string representation of session for current user"""
|
||||
return "%s" % context.session
|
||||
|
||||
def getSessionInfo(self):
|
||||
"""Return session info for current user"""
|
||||
def getSessionInfo(self, details=False, user_id=None):
|
||||
"""Return session info for current user or all not expired sessions to specific user
|
||||
|
||||
:param boolean details: add session ID and hostip to result
|
||||
:param str user_id: show all not expired sessions related to specific user
|
||||
|
||||
:returns: dict or list of dicts session data
|
||||
"""
|
||||
if not context.session.logged_in:
|
||||
return None
|
||||
return context.session.session_data
|
||||
clauses = ['expired is FALSE']
|
||||
fields = {'user_id': 'user_id',
|
||||
'expired': 'expired',
|
||||
'master': 'master',
|
||||
'authtype': 'authtype',
|
||||
'callnum': 'callnum',
|
||||
"date_part('epoch', start_time)": 'start_time',
|
||||
'update_time': 'update_time',
|
||||
'exclusive': 'exclusive',
|
||||
}
|
||||
columns, aliases = zip(*fields.items())
|
||||
if details:
|
||||
columns += ('hostip', 'id')
|
||||
aliases += ('hostip', 'id')
|
||||
if user_id:
|
||||
user_id = get_user(user_id, strict=True)['id']
|
||||
logged_user_id = self.getLoggedInUser()['id']
|
||||
if not context.session.hasPerm('admin') and user_id != logged_user_id:
|
||||
raise koji.ActionNotAllowed('only admins or owners may see all active sessions')
|
||||
clauses.append('user_id = %(user_id)i')
|
||||
else:
|
||||
result = context.session.session_data
|
||||
if details:
|
||||
id = context.session.id
|
||||
clauses.append('id = %(id)i')
|
||||
else:
|
||||
return result
|
||||
query = QueryProcessor(tables=['sessions'],
|
||||
columns=columns, aliases=aliases,
|
||||
clauses=clauses,
|
||||
values=locals())
|
||||
if details and not user_id:
|
||||
result_query = query.executeOne()
|
||||
result['hostip'] = result_query['hostip']
|
||||
result['id'] = result_query['id']
|
||||
else:
|
||||
result = query.execute()
|
||||
return result
|
||||
|
||||
def showOpts(self, as_string=True):
|
||||
"""Returns hub options
|
||||
|
|
|
|||
|
|
@ -2644,13 +2644,13 @@ class ClientSession(object):
|
|||
self.authtype = AUTHTYPES['SSL']
|
||||
return True
|
||||
|
||||
def logout(self):
|
||||
def logout(self, session_id=None):
|
||||
if not self.logged_in:
|
||||
return
|
||||
try:
|
||||
# bypass _callMethod (no retries)
|
||||
# XXX - is that really what we want?
|
||||
handler, headers, request = self._prepCall('logout', ())
|
||||
handler, headers, request = self._prepCall('logout', (), {"session_id": session_id})
|
||||
self._sendCall(handler, headers, request)
|
||||
except AuthExpired:
|
||||
# this can happen when an exclusive session is forced
|
||||
|
|
|
|||
22
koji/auth.py
22
koji/auth.py
|
|
@ -443,17 +443,29 @@ class Session(object):
|
|||
update.execute()
|
||||
context.cnx.commit()
|
||||
|
||||
def logout(self):
|
||||
def logout(self, session_id=None):
|
||||
"""expire a login session"""
|
||||
if not self.logged_in:
|
||||
# XXX raise an error?
|
||||
raise koji.AuthError("Not logged in")
|
||||
|
||||
if session_id:
|
||||
if not context.session.hasPerm('admin'):
|
||||
query = QueryProcessor(tables=['sessions'], columns=['id'],
|
||||
clauses=['user_id = %(user_id)i', 'id = %(session_id)s'],
|
||||
values={'user_id': self.user_id, 'session_id': session_id})
|
||||
if not query.singleValue():
|
||||
raise koji.ActionNotAllowed('only admins or owner may logout other session')
|
||||
ses_id = session_id
|
||||
else:
|
||||
ses_id = self.id
|
||||
update = UpdateProcessor('sessions', data={'expired': True, 'exclusive': None},
|
||||
clauses=['id = %(id)i OR master = %(id)i'],
|
||||
values={'id': self.id})
|
||||
values={'id': ses_id})
|
||||
update.execute()
|
||||
context.cnx.commit()
|
||||
self.logged_in = False
|
||||
if not session_id:
|
||||
self.logged_in = False
|
||||
|
||||
def logoutChild(self, session_id):
|
||||
"""expire a subsession"""
|
||||
|
|
@ -749,9 +761,9 @@ def sslLogin(*args, **opts):
|
|||
return context.session.sslLogin(*args, **opts)
|
||||
|
||||
|
||||
def logout():
|
||||
def logout(session_id=None):
|
||||
"""expire a login session"""
|
||||
return context.session.logout()
|
||||
return context.session.logout(session_id)
|
||||
|
||||
|
||||
def subsession():
|
||||
|
|
|
|||
107
tests/test_hub/test_get_session_info.py
Normal file
107
tests/test_hub/test_get_session_info.py
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import mock
|
||||
import unittest
|
||||
|
||||
import koji
|
||||
import kojihub
|
||||
|
||||
QP = kojihub.QueryProcessor
|
||||
|
||||
|
||||
class TestGetSessionInfo(unittest.TestCase):
|
||||
def getQuery(self, *args, **kwargs):
|
||||
query = QP(*args, **kwargs)
|
||||
query.execute = mock.MagicMock()
|
||||
self.queries.append(query)
|
||||
return query
|
||||
|
||||
def setUp(self):
|
||||
self.context = mock.patch('kojihub.context').start()
|
||||
self.exports = kojihub.RootExports()
|
||||
|
||||
self.QueryProcessor = mock.patch('kojihub.QueryProcessor',
|
||||
side_effect=self.getQuery).start()
|
||||
self.queries = []
|
||||
self.context.session.hasPerm = mock.MagicMock()
|
||||
self.get_user = mock.patch('kojihub.get_user').start()
|
||||
self.userinfo = {'id': 123, 'name': 'testuser'}
|
||||
self.exports.getLoggedInUser = mock.MagicMock()
|
||||
|
||||
def tearDown(self):
|
||||
mock.patch.stopall()
|
||||
|
||||
def test_get_session_info_not_logged(self):
|
||||
self.context.session.logged_in = False
|
||||
result = self.exports.getSessionInfo()
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_get_session_info_user_not_admin_and_not_logged_user(self):
|
||||
self.context.session.logged_in = True
|
||||
self.context.session.hasPerm.return_value = False
|
||||
self.get_user.return_value = self.userinfo
|
||||
self.exports.getLoggedInUser.return_value = {'id': 159, 'name': 'testuser2'}
|
||||
with self.assertRaises(koji.ActionNotAllowed) as ex:
|
||||
self.exports.getSessionInfo(user_id='testuser')
|
||||
self.assertEqual("only admins or owners may see all active sessions", str(ex.exception))
|
||||
self.assertEqual(len(self.queries), 0)
|
||||
|
||||
def test_get_session_info_user_logged_user(self):
|
||||
self.context.session.logged_in = True
|
||||
self.context.session.hasPerm.return_value = False
|
||||
self.get_user.return_value = self.userinfo
|
||||
self.exports.getLoggedInUser.return_value = {'id': 123, 'name': 'testuser'}
|
||||
self.exports.getSessionInfo(user_id='testuser')
|
||||
self.assertEqual(len(self.queries), 1)
|
||||
query = self.queries[0]
|
||||
self.assertEqual(query.tables, ['sessions'])
|
||||
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'])
|
||||
self.assertEqual(query.aliases, ['authtype', 'callnum', 'exclusive', 'expired', 'master',
|
||||
'start_time', 'update_time', 'user_id'])
|
||||
|
||||
def test_get_session_info_user_and_details(self):
|
||||
self.context.session.logged_in = True
|
||||
self.context.session.hasPerm.return_value = True
|
||||
self.exports.getSessionInfo(details=True, user_id='testuser')
|
||||
self.assertEqual(len(self.queries), 1)
|
||||
query = self.queries[0]
|
||||
self.assertEqual(query.tables, ['sessions'])
|
||||
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', 'hostip',
|
||||
'id', 'master', "date_part('epoch', start_time)",
|
||||
'update_time', 'user_id'])
|
||||
self.assertEqual(query.aliases, ['authtype', 'callnum', 'exclusive', 'expired', 'hostip',
|
||||
'id', 'master', 'start_time', 'update_time', 'user_id'])
|
||||
|
||||
def test_get_session_info_user(self):
|
||||
self.context.session.logged_in = True
|
||||
self.context.session.hasPerm.return_value = True
|
||||
self.exports.getSessionInfo(user_id='testuser')
|
||||
self.assertEqual(len(self.queries), 1)
|
||||
query = self.queries[0]
|
||||
self.assertEqual(query.tables, ['sessions'])
|
||||
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'])
|
||||
self.assertEqual(query.aliases, ['authtype', 'callnum', 'exclusive', 'expired', 'master',
|
||||
'start_time', 'update_time', 'user_id'])
|
||||
|
||||
def test_get_session_info_details(self):
|
||||
self.context.session.logged_in = True
|
||||
self.context.session.hasPerm.return_value = True
|
||||
self.exports.getSessionInfo(details=True)
|
||||
self.assertEqual(len(self.queries), 1)
|
||||
query = self.queries[0]
|
||||
self.assertEqual(query.tables, ['sessions'])
|
||||
self.assertEqual(query.clauses, ['expired is FALSE', 'id = %(id)i'])
|
||||
self.assertEqual(query.joins, None)
|
||||
self.assertEqual(query.columns, ['authtype', 'callnum', 'exclusive', 'expired', 'hostip',
|
||||
'id', 'master', "date_part('epoch', start_time)",
|
||||
'update_time', 'user_id'])
|
||||
self.assertEqual(query.aliases, ['authtype', 'callnum', 'exclusive', 'expired', 'hostip',
|
||||
'id', 'master', 'start_time', 'update_time', 'user_id'])
|
||||
|
|
@ -624,3 +624,25 @@ class TestAuthSession(unittest.TestCase):
|
|||
self.assertEqual(query.joins, ['permissions ON perm_id = permissions.id'])
|
||||
self.assertEqual(query.clauses, ['active = TRUE', 'user_id=%(user_id)s'])
|
||||
self.assertEqual(query.columns, ['name'])
|
||||
|
||||
def test_logout_not_logged(self):
|
||||
s, cntext = self.get_session()
|
||||
|
||||
# not logged
|
||||
s.logged_in = False
|
||||
with self.assertRaises(koji.AuthError) as ex:
|
||||
s.logout()
|
||||
self.assertEqual("Not logged in", str(ex.exception))
|
||||
|
||||
@mock.patch('koji.auth.context')
|
||||
def test_logout_logged_not_owner(self, context):
|
||||
s, cntext = self.get_session()
|
||||
|
||||
s.logged_in = True
|
||||
# session_id without admin perms and not owner
|
||||
context.session.hasPerm.return_value = False
|
||||
context.session.user_id.return_value = 123
|
||||
self.query_singleValue.return_value = None
|
||||
with self.assertRaises(koji.ActionNotAllowed) as ex:
|
||||
s.logout(session_id=1)
|
||||
self.assertEqual("only admins or owner may logout other session", str(ex.exception))
|
||||
|
|
|
|||
28
www/kojiweb/activesession.chtml
Normal file
28
www/kojiweb/activesession.chtml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#include "includes/header.chtml"
|
||||
#import koji
|
||||
#from kojiweb import util
|
||||
|
||||
#attr _PASSTHROUGH = ['userID']
|
||||
|
||||
<h4>Active sessions for $loggedInUser.name user</h4>
|
||||
<br>
|
||||
<table class="data-list">
|
||||
<tr class="list-header">
|
||||
<th><a href="activesession?order=$util.toggleOrder($self, 'id')$util.passthrough_except($self, 'order')">Session ID</a> $util.sortImage($self, 'id')</th>
|
||||
<th><a href="activesession?order=$util.toggleOrder($self, 'hostip')$util.passthrough_except($self, 'order')">Client IP</a> $util.sortImage($self, 'hostip')</th>
|
||||
<th><a href="activesession?order=$util.toggleOrder($self, 'authtype')$util.passthrough_except($self, 'order')">Auth type</a> $util.sortImage($self, 'authtype')</th>
|
||||
<th><a href="activesession?order=$util.toggleOrder($self, 'start_time')$util.passthrough_except($self, 'order')">Session start time</a> $util.sortImage($self, 'start_time')</th>
|
||||
<th><a href="activesession?order=$util.toggleOrder($self, 'start_time')$util.passthrough_except($self, 'order')">Length session</a> $util.sortImage($self, 'start_time')</th>
|
||||
<th><a href="activesession?order=$util.toggleOrder($self, 'id')$util.passthrough_except($self, 'order')">Logout?</a> $util.sortImage($self, 'id')</th>
|
||||
</tr>
|
||||
#for $act in $activesess
|
||||
<tr class="$util.rowToggle($self)">
|
||||
<td>$act.id</td>
|
||||
<td>$util.escapeHTML($act.hostip)</td>
|
||||
<td>$act.authtype</td>
|
||||
<td>$util.formatTimeLong($act.start_time)</td>
|
||||
<td>$act.lengthSession days</td>
|
||||
<td><a href="activesessiondelete?sessionID=$act.id$util.authToken($self)">Logout</a></td>
|
||||
</tr>
|
||||
#end for
|
||||
</table>
|
||||
|
|
@ -2135,7 +2135,11 @@ def buildtargetdelete(environ, targetID):
|
|||
|
||||
def reports(environ):
|
||||
_getServer(environ)
|
||||
_initValues(environ, 'Reports', 'reports')
|
||||
values = _initValues(environ, 'Reports', 'reports')
|
||||
if environ['koji.currentUser']:
|
||||
values['loggedInUser'] = True
|
||||
else:
|
||||
values['loggedInUser'] = False
|
||||
return _genHTML(environ, 'reports.chtml')
|
||||
|
||||
|
||||
|
|
@ -2656,3 +2660,35 @@ def repoinfo(environ, repoID):
|
|||
values['repo_json'] = os.path.join(
|
||||
pathinfo.repo(repo_info['id'], repo_info['tag_name']), 'repo.json')
|
||||
return _genHTML(environ, 'repoinfo.chtml')
|
||||
|
||||
|
||||
def activesession(environ, start=None, order=None):
|
||||
values = _initValues(environ, 'Active sessions', 'activesession')
|
||||
server = _getServer(environ)
|
||||
|
||||
values['loggedInUser'] = environ['koji.currentUser']
|
||||
|
||||
values['order'] = order
|
||||
activesess = server.getSessionInfo(details=True, user_id=values['loggedInUser']['id'])
|
||||
if not activesess:
|
||||
activesess = []
|
||||
else:
|
||||
current_timestamp = datetime.datetime.utcnow().timestamp()
|
||||
for a in activesess:
|
||||
a['lengthSession'] = kojiweb.util.formatTimestampDifference(
|
||||
a['start_time'], current_timestamp, in_days=True)
|
||||
|
||||
kojiweb.util.paginateList(values, activesess, start, 'activesess', order=order)
|
||||
|
||||
return _genHTML(environ, 'activesession.chtml')
|
||||
|
||||
|
||||
def activesessiondelete(environ, sessionID):
|
||||
server = _getServer(environ)
|
||||
_assertLogin(environ)
|
||||
|
||||
sessionID = int(sessionID)
|
||||
|
||||
server.logout(session_id=sessionID)
|
||||
|
||||
_redirect(environ, 'activesession')
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@
|
|||
<li><a href="buildsbystatus">Succeeded/failed/canceled builds</a></li>
|
||||
<li><a href="buildsbytarget">Number of builds in each target</a></li>
|
||||
<li><a href="clusterhealth">Cluster health</a></li>
|
||||
#if $loggedInUser
|
||||
<li><a href="activesession">Active sessions</a></li>
|
||||
#end if
|
||||
</ul>
|
||||
|
||||
#include "includes/footer.chtml"
|
||||
|
|
|
|||
|
|
@ -469,13 +469,15 @@ formatTimeRSS = koji.formatTimeLong
|
|||
formatTimeLong = koji.formatTimeLong
|
||||
|
||||
|
||||
def formatTimestampDifference(start_ts, end_ts):
|
||||
def formatTimestampDifference(start_ts, end_ts, in_days=False):
|
||||
diff = end_ts - start_ts
|
||||
seconds = diff % 60
|
||||
diff = diff // 60
|
||||
minutes = diff % 60
|
||||
diff = diff // 60
|
||||
hours = diff
|
||||
if in_days:
|
||||
return round(hours / 24, 1)
|
||||
return "%d:%02d:%02d" % (hours, minutes, seconds)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue