##// END OF EJS Templates
admin-users: add audit page to allow showing user actions in RhodeCode....
marcink -
r1559:6a97fe2f default
parent child Browse files
Show More
@@ -0,0 +1,112 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import logging
22
23 from whoosh.qparser.default import QueryParser, query
24 from whoosh.qparser.dateparse import DateParserPlugin
25 from whoosh.fields import (TEXT, Schema, DATETIME)
26 from sqlalchemy.sql.expression import or_, and_, func
27
28 from rhodecode.model.db import UserLog
29 from rhodecode.lib.utils2 import remove_prefix, remove_suffix
30
31 # JOURNAL SCHEMA used only to generate queries in journal. We use whoosh
32 # querylang to build sql queries and filter journals
33 JOURNAL_SCHEMA = Schema(
34 username=TEXT(),
35 date=DATETIME(),
36 action=TEXT(),
37 repository=TEXT(),
38 ip=TEXT(),
39 )
40
41 log = logging.getLogger(__name__)
42
43
44 def user_log_filter(user_log, search_term):
45 """
46 Filters sqlalchemy user_log based on search_term with whoosh Query language
47 http://packages.python.org/Whoosh/querylang.html
48
49 :param user_log:
50 :param search_term:
51 """
52 log.debug('Initial search term: %r' % search_term)
53 qry = None
54 if search_term:
55 qp = QueryParser('repository', schema=JOURNAL_SCHEMA)
56 qp.add_plugin(DateParserPlugin())
57 qry = qp.parse(unicode(search_term))
58 log.debug('Filtering using parsed query %r' % qry)
59
60 def wildcard_handler(col, wc_term):
61 if wc_term.startswith('*') and not wc_term.endswith('*'):
62 # postfix == endswith
63 wc_term = remove_prefix(wc_term, prefix='*')
64 return func.lower(col).endswith(wc_term)
65 elif wc_term.startswith('*') and wc_term.endswith('*'):
66 # wildcard == ilike
67 wc_term = remove_prefix(wc_term, prefix='*')
68 wc_term = remove_suffix(wc_term, suffix='*')
69 return func.lower(col).contains(wc_term)
70
71 def get_filterion(field, val, term):
72
73 if field == 'repository':
74 field = getattr(UserLog, 'repository_name')
75 elif field == 'ip':
76 field = getattr(UserLog, 'user_ip')
77 elif field == 'date':
78 field = getattr(UserLog, 'action_date')
79 elif field == 'username':
80 field = getattr(UserLog, 'username')
81 else:
82 field = getattr(UserLog, field)
83 log.debug('filter field: %s val=>%s' % (field, val))
84
85 # sql filtering
86 if isinstance(term, query.Wildcard):
87 return wildcard_handler(field, val)
88 elif isinstance(term, query.Prefix):
89 return func.lower(field).startswith(func.lower(val))
90 elif isinstance(term, query.DateRange):
91 return and_(field >= val[0], field <= val[1])
92 return func.lower(field) == func.lower(val)
93
94 if isinstance(qry, (query.And, query.Term, query.Prefix, query.Wildcard,
95 query.DateRange)):
96 if not isinstance(qry, query.And):
97 qry = [qry]
98 for term in qry:
99 field = term.fieldname
100 val = (term.text if not isinstance(term, query.DateRange)
101 else [term.startdate, term.enddate])
102 user_log = user_log.filter(get_filterion(field, val, term))
103 elif isinstance(qry, query.Or):
104 filters = []
105 for term in qry:
106 field = term.fieldname
107 val = (term.text if not isinstance(term, query.DateRange)
108 else [term.startdate, term.enddate])
109 filters.append(get_filterion(field, val, term))
110 user_log = user_log.filter(or_(*filters))
111
112 return user_log
@@ -0,0 +1,65 b''
1 ## -*- coding: utf-8 -*-
2 <%namespace name="base" file="/base/base.mako"/>
3
4
5 <div class="panel panel-default">
6 <div class="panel-heading">
7 <h3 class="panel-title">${_('User Audit Logs')} -
8 ${_ungettext('%s entry', '%s entries', c.user_log.item_count) % (c.user_log.item_count)}
9 </h3>
10 </div>
11 <div class="panel-body">
12
13 ${h.form(None, id_="filter_form", method="get")}
14 <input class="q_filter_box ${'' if c.filter_term else 'initial'}" id="j_filter" size="15" type="text" name="filter" value="${c.filter_term or ''}" placeholder="${_('audit filter...')}"/>
15 <input type='submit' value="${_('filter')}" class="btn" />
16 ${h.end_form()}
17 <p class="tooltip filterexample" style="position: inherit" title="${h.tooltip(h.journal_filter_help())}">${_('Example Queries')}</p>
18
19 % if c.user_log:
20 <table class="rctable admin_log">
21 <tr>
22 <th>${_('Username')}</th>
23 <th>${_('Action')}</th>
24 <th>${_('Repository')}</th>
25 <th>${_('Date')}</th>
26 <th>${_('From IP')}</th>
27 </tr>
28
29 %for cnt,l in enumerate(c.user_log):
30 <tr class="parity${cnt%2}">
31 <td class="td-user">
32 %if l.user is not None:
33 ${base.gravatar_with_user(l.user.email)}
34 %else:
35 ${l.username}
36 %endif
37 </td>
38 <td class="td-journalaction">${h.action_parser(l)[0]()}
39 <div class="journal_action_params">
40 ${h.literal(h.action_parser(l)[1]())}
41 </div>
42 </td>
43 <td class="td-componentname">
44 %if l.repository is not None:
45 ${h.link_to(l.repository.repo_name,h.url('summary_home',repo_name=l.repository.repo_name))}
46 %else:
47 ${l.repository_name}
48 %endif
49 </td>
50
51 <td class="td-time">${h.format_date(l.action_date)}</td>
52 <td class="td-ip">${l.user_ip}</td>
53 </tr>
54 %endfor
55 </table>
56
57 <div class="pagination-wh pagination-left">
58 ${c.user_log.pager('$link_previous ~2~ $link_next')}
59 </div>
60 % else:
61 ${_('No actions yet')}
62 % endif
63
64 </div>
65 </div>
@@ -79,6 +79,11 b' def admin_routes(config):'
79 79 name='edit_user_groups_management_updates',
80 80 pattern='/users/{user_id:\d+}/edit/edit_user_groups_management/updates')
81 81
82 # user audit logs
83 config.add_route(
84 name='edit_user_audit_logs',
85 pattern='/users/{user_id:\d+}/edit/audit')
86
82 87
83 88 def includeme(config):
84 89 settings = config.get_settings()
@@ -22,6 +22,8 b' import logging'
22 22
23 23 from pyramid.httpexceptions import HTTPFound
24 24 from pyramid.view import view_config
25
26 from rhodecode.lib.helpers import Page
25 27 from rhodecode_tools.lib.ext_json import json
26 28
27 29 from rhodecode.apps._base import BaseAppView
@@ -31,6 +33,7 b' from rhodecode.lib import helpers as h'
31 33 from rhodecode.lib.utils import PartialRenderer
32 34 from rhodecode.lib.utils2 import safe_int, safe_unicode
33 35 from rhodecode.model.auth_token import AuthTokenModel
36 from rhodecode.model.user import UserModel
34 37 from rhodecode.model.user_group import UserGroupModel
35 38 from rhodecode.model.db import User, or_
36 39 from rhodecode.model.meta import Session
@@ -238,7 +241,6 b' class AdminUsersView(BaseAppView):'
238 241
239 242 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
240 243
241
242 244 @LoginRequired()
243 245 @HasPermissionAllDecorator('hg.admin')
244 246 @view_config(
@@ -257,7 +259,6 b' class AdminUsersView(BaseAppView):'
257 259
258 260 return self._get_template_context(c)
259 261
260
261 262 @LoginRequired()
262 263 @HasPermissionAllDecorator('hg.admin')
263 264 @view_config(
@@ -282,4 +283,35 b' class AdminUsersView(BaseAppView):'
282 283 c.active = 'user_groups_management'
283 284 h.flash(_("Groups successfully changed"), category='success')
284 285
285 return HTTPFound(h.route_path('edit_user_groups_management', user_id=user_id))
286 return HTTPFound(h.route_path(
287 'edit_user_groups_management', user_id=user_id))
288
289 @LoginRequired()
290 @HasPermissionAllDecorator('hg.admin')
291 @view_config(
292 route_name='edit_user_audit_logs', request_method='GET',
293 renderer='rhodecode:templates/admin/users/user_edit.mako')
294 def user_audit_logs(self):
295 _ = self.request.translate
296 c = self.load_default_context()
297
298 user_id = self.request.matchdict.get('user_id')
299 c.user = User.get_or_404(user_id, pyramid_exc=True)
300 self._redirect_for_default_user(c.user.username)
301 c.active = 'audit'
302
303 p = safe_int(self.request.GET.get('page', 1), 1)
304
305 filter_term = self.request.GET.get('filter')
306 c.user_log = UserModel().get_user_log(c.user, filter_term)
307
308 def url_generator(**kw):
309 if filter_term:
310 kw['filter'] = filter_term
311 return self.request.current_route_path(_query=kw)
312
313 c.user_log = Page(c.user_log, page=p, items_per_page=10,
314 url=url_generator)
315 c.filter_term = filter_term
316 return self._get_template_context(c)
317
@@ -28,101 +28,17 b' import logging'
28 28 from pylons import request, tmpl_context as c, url
29 29 from pylons.controllers.util import redirect
30 30 from sqlalchemy.orm import joinedload
31 from whoosh.qparser.default import QueryParser, query
32 from whoosh.qparser.dateparse import DateParserPlugin
33 from whoosh.fields import (TEXT, Schema, DATETIME)
34 from sqlalchemy.sql.expression import or_, and_, func
35 31
36 32 from rhodecode.model.db import UserLog, PullRequest
33 from rhodecode.lib.user_log_filter import user_log_filter
37 34 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
38 35 from rhodecode.lib.base import BaseController, render
39 from rhodecode.lib.utils2 import safe_int, remove_prefix, remove_suffix
36 from rhodecode.lib.utils2 import safe_int
40 37 from rhodecode.lib.helpers import Page
41 38
42 39
43 40 log = logging.getLogger(__name__)
44 41
45 # JOURNAL SCHEMA used only to generate queries in journal. We use whoosh
46 # querylang to build sql queries and filter journals
47 JOURNAL_SCHEMA = Schema(
48 username=TEXT(),
49 date=DATETIME(),
50 action=TEXT(),
51 repository=TEXT(),
52 ip=TEXT(),
53 )
54
55
56 def _journal_filter(user_log, search_term):
57 """
58 Filters sqlalchemy user_log based on search_term with whoosh Query language
59 http://packages.python.org/Whoosh/querylang.html
60
61 :param user_log:
62 :param search_term:
63 """
64 log.debug('Initial search term: %r' % search_term)
65 qry = None
66 if search_term:
67 qp = QueryParser('repository', schema=JOURNAL_SCHEMA)
68 qp.add_plugin(DateParserPlugin())
69 qry = qp.parse(unicode(search_term))
70 log.debug('Filtering using parsed query %r' % qry)
71
72 def wildcard_handler(col, wc_term):
73 if wc_term.startswith('*') and not wc_term.endswith('*'):
74 # postfix == endswith
75 wc_term = remove_prefix(wc_term, prefix='*')
76 return func.lower(col).endswith(wc_term)
77 elif wc_term.startswith('*') and wc_term.endswith('*'):
78 # wildcard == ilike
79 wc_term = remove_prefix(wc_term, prefix='*')
80 wc_term = remove_suffix(wc_term, suffix='*')
81 return func.lower(col).contains(wc_term)
82
83 def get_filterion(field, val, term):
84
85 if field == 'repository':
86 field = getattr(UserLog, 'repository_name')
87 elif field == 'ip':
88 field = getattr(UserLog, 'user_ip')
89 elif field == 'date':
90 field = getattr(UserLog, 'action_date')
91 elif field == 'username':
92 field = getattr(UserLog, 'username')
93 else:
94 field = getattr(UserLog, field)
95 log.debug('filter field: %s val=>%s' % (field, val))
96
97 # sql filtering
98 if isinstance(term, query.Wildcard):
99 return wildcard_handler(field, val)
100 elif isinstance(term, query.Prefix):
101 return func.lower(field).startswith(func.lower(val))
102 elif isinstance(term, query.DateRange):
103 return and_(field >= val[0], field <= val[1])
104 return func.lower(field) == func.lower(val)
105
106 if isinstance(qry, (query.And, query.Term, query.Prefix, query.Wildcard,
107 query.DateRange)):
108 if not isinstance(qry, query.And):
109 qry = [qry]
110 for term in qry:
111 field = term.fieldname
112 val = (term.text if not isinstance(term, query.DateRange)
113 else [term.startdate, term.enddate])
114 user_log = user_log.filter(get_filterion(field, val, term))
115 elif isinstance(qry, query.Or):
116 filters = []
117 for term in qry:
118 field = term.fieldname
119 val = (term.text if not isinstance(term, query.DateRange)
120 else [term.startdate, term.enddate])
121 filters.append(get_filterion(field, val, term))
122 user_log = user_log.filter(or_(*filters))
123
124 return user_log
125
126 42
127 43 class AdminController(BaseController):
128 44
@@ -139,7 +55,7 b' class AdminController(BaseController):'
139 55 # FILTERING
140 56 c.search_term = request.GET.get('filter')
141 57 try:
142 users_log = _journal_filter(users_log, c.search_term)
58 users_log = user_log_filter(users_log, c.search_term)
143 59 except Exception:
144 60 # we want this to crash for now
145 61 raise
@@ -34,7 +34,7 b' from webob.exc import HTTPBadRequest'
34 34 from pylons import request, tmpl_context as c, response, url
35 35 from pylons.i18n.translation import _
36 36
37 from rhodecode.controllers.admin.admin import _journal_filter
37 from rhodecode.controllers.admin.admin import user_log_filter
38 38 from rhodecode.model.db import UserLog, UserFollowing, User, UserApiKeys
39 39 from rhodecode.model.meta import Session
40 40 import rhodecode.lib.helpers as h
@@ -89,7 +89,7 b' class JournalController(BaseController):'
89 89 .options(joinedload(UserLog.repository))
90 90 #filter
91 91 try:
92 journal = _journal_filter(journal, c.search_term)
92 journal = user_log_filter(journal, c.search_term)
93 93 except Exception:
94 94 # we want this to crash for now
95 95 raise
@@ -33,6 +33,7 b' from sqlalchemy.exc import DatabaseError'
33 33 from sqlalchemy.sql.expression import true, false
34 34
35 35 from rhodecode import events
36 from rhodecode.lib.user_log_filter import user_log_filter
36 37 from rhodecode.lib.utils2 import (
37 38 safe_unicode, get_current_rhodecode_user, action_logger_generic,
38 39 AttributeDict, str2bool)
@@ -40,7 +41,7 b' from rhodecode.lib.caching_query import '
40 41 from rhodecode.model import BaseModel
41 42 from rhodecode.model.auth_token import AuthTokenModel
42 43 from rhodecode.model.db import (
43 User, UserToPerm, UserEmailMap, UserIpMap)
44 or_, joinedload, User, UserToPerm, UserEmailMap, UserIpMap, UserLog)
44 45 from rhodecode.lib.exceptions import (
45 46 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
46 47 UserOwnsUserGroupsException, NotAllowedToCreateUserError)
@@ -847,3 +848,14 b' class UserModel(BaseModel):'
847 848 Session().commit()
848 849
849 850 return
851
852 def get_user_log(self, user, filter_term):
853 user_log = UserLog.query()\
854 .filter(or_(UserLog.user_id == user.user_id,
855 UserLog.username == user.username))\
856 .options(joinedload(UserLog.user))\
857 .options(joinedload(UserLog.repository))\
858 .order_by(UserLog.action_date.desc())
859
860 user_log = user_log_filter(user_log, filter_term)
861 return user_log
@@ -37,11 +37,8 b''
37 37 <li class="${'active' if c.active=='perms_summary' else ''}"><a href="${h.url('edit_user_perms_summary', user_id=c.user.user_id)}">${_('Permissions summary')}</a></li>
38 38 <li class="${'active' if c.active=='emails' else ''}"><a href="${h.url('edit_user_emails', user_id=c.user.user_id)}">${_('Emails')}</a></li>
39 39 <li class="${'active' if c.active=='ips' else ''}"><a href="${h.url('edit_user_ips', user_id=c.user.user_id)}">${_('Ip Whitelist')}</a></li>
40
41 <li class="${'active' if c.active=='groups' else ''}">
42 <a href="${h.route_path('edit_user_groups_management', user_id=c.user.user_id)}">${_('User Groups Management')}</a>
43 </li>
44
40 <li class="${'active' if c.active=='groups' else ''}"><a href="${h.route_path('edit_user_groups_management', user_id=c.user.user_id)}">${_('User Groups Management')}</a></li>
41 <li class="${'active' if c.active=='audit' else ''}"><a href="${h.route_path('edit_user_audit_logs', user_id=c.user.user_id)}">${_('User audit')}</a></li>
45 42 </ul>
46 43 </div>
47 44
General Comments 0
You need to be logged in to leave comments. Login now