#3446 Add active sessions web page
Merged 2 years ago by tkopecek. Opened 2 years ago by jcupova.
jcupova/koji issue-3396  into  master

Add active sessions web page
Jana Cupova • 2 years ago  
file modified
+46 -3
@@ -10257,11 +10257,54 @@ 

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

file modified
+2 -2
@@ -2644,13 +2644,13 @@ 

          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

file modified
+17 -5
@@ -443,17 +443,29 @@ 

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

      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():

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

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

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

file modified
+37 -1
@@ -2135,7 +2135,11 @@ 

  

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

              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"

file modified
+3 -1
@@ -469,13 +469,15 @@ 

  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)

  

  

rebased onto 95b56b3f574402cdf6f3e16a9d33c23794b7a0c5

2 years ago

Just name it consistently with logged_user_id as user_id.

and -> or (otherwise admin will not be able to get info about other users)

Why it should be filtered on start_time in this case?

It is a dict or list of dicts.

This can be retrieved directly from query date_part('epoch', start_time)

It is auser which kojiweb uses to auth. Get proxied user via environ['koji.currentUser'] (E.g. when I'm not logged in in koji-dev env, it will still return kojiadmin user)

AND expired IS TRUE AND id = %(session_id)s - it is returning 0/1 records. (Or alternatively use EXISTS for bool check) We should also rewrite it to QueryProcessor when we're here (same for next query).

rebased onto ed89b8725d30b90d74c6e48d3e3129b4220b32b4

2 years ago

rebased onto 12f173b7553fd76688225940dcf29e8eee755dd7

2 years ago

rebased onto 01e68ddafe7094ae559495696af06bf043ef8566

2 years ago

rebased onto 6bf759953f0843538f414cf74ecfb320a7a1924e

2 years ago

It should be a default clause - we don't care about expired sessions, they should be handled by GC not this call.

rebased onto a8bbeef2f452c518a1c7413677f905f650fac26b

2 years ago

Metadata Update from @tkopecek:
- Pull-request tagged with: testing-ready

2 years ago

rebased onto 5984b612a460455f8f6fcfafaf450151e585ef32

2 years ago

rebased onto 07c325e47e4427778959be17e44fc2f15470f0a4

2 years ago

rebased onto 8ca74441610e3461427cf766c6c1dd4365dc3b3d

2 years ago

Metadata Update from @jobrauer:
- Pull-request untagged with: testing-ready

2 years ago

testing-ready removed due to merge conflict on our test branch

rebased onto 17d3e730ab5b0220863b3a513e7eea58c7dc2e86

2 years ago

rebased onto 151bf7414601e7acf945259603e685ad42aa80e3

2 years ago

rebased onto 87b16370498007b6c7e151eb68d561b2f97a512a

2 years ago

rebased onto 2eee0c87206b9df2399b54e5c6aee8fbac254de9

2 years ago

Metadata Update from @jcupova:
- Pull-request tagged with: testing-ready

2 years ago

rebased onto 86d9282

2 years ago

Metadata Update from @jobrauer:
- Pull-request tagged with: testing-done

2 years ago

Commit 08a9fb8 fixes this pull-request

Pull-Request has been merged by tkopecek

2 years ago