admin-users: add audit page to allow showing user actions in RhodeCode....
marcink -
r1559:6a97fe2f default
Not Reviewed
Show More
Add another comment
TODOs: 0 unresolved 0 Resolved
COMMENTS: 0 General 0 Inline
@@ -0,0 +1,112
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
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
79 name='edit_user_groups_management_updates',
79 name='edit_user_groups_management_updates',
80 pattern='/users/{user_id:\d+}/edit/edit_user_groups_management/updates')
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 def includeme(config):
88 def includeme(config):
84 settings = config.get_settings()
89 settings = config.get_settings()
@@ -22,6 +22,8
22
22
23 from pyramid.httpexceptions import HTTPFound
23 from pyramid.httpexceptions import HTTPFound
24 from pyramid.view import view_config
24 from pyramid.view import view_config
25
26 from rhodecode.lib.helpers import Page
25 from rhodecode_tools.lib.ext_json import json
27 from rhodecode_tools.lib.ext_json import json
26
28
27 from rhodecode.apps._base import BaseAppView
29 from rhodecode.apps._base import BaseAppView
@@ -31,6 +33,7
31 from rhodecode.lib.utils import PartialRenderer
33 from rhodecode.lib.utils import PartialRenderer
32 from rhodecode.lib.utils2 import safe_int, safe_unicode
34 from rhodecode.lib.utils2 import safe_int, safe_unicode
33 from rhodecode.model.auth_token import AuthTokenModel
35 from rhodecode.model.auth_token import AuthTokenModel
36 from rhodecode.model.user import UserModel
34 from rhodecode.model.user_group import UserGroupModel
37 from rhodecode.model.user_group import UserGroupModel
35 from rhodecode.model.db import User, or_
38 from rhodecode.model.db import User, or_
36 from rhodecode.model.meta import Session
39 from rhodecode.model.meta import Session
@@ -238,7 +241,6
238
241
239 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
242 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
240
243
241
242 @LoginRequired()
244 @LoginRequired()
243 @HasPermissionAllDecorator('hg.admin')
245 @HasPermissionAllDecorator('hg.admin')
244 @view_config(
246 @view_config(
@@ -257,7 +259,6
257
259
258 return self._get_template_context(c)
260 return self._get_template_context(c)
259
261
260
261 @LoginRequired()
262 @LoginRequired()
262 @HasPermissionAllDecorator('hg.admin')
263 @HasPermissionAllDecorator('hg.admin')
263 @view_config(
264 @view_config(
@@ -282,4 +283,35
282 c.active = 'user_groups_management'
283 c.active = 'user_groups_management'
283 h.flash(_("Groups successfully changed"), category='success')
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
28 from pylons import request, tmpl_context as c, url
28 from pylons import request, tmpl_context as c, url
29 from pylons.controllers.util import redirect
29 from pylons.controllers.util import redirect
30 from sqlalchemy.orm import joinedload
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 from rhodecode.model.db import UserLog, PullRequest
32 from rhodecode.model.db import UserLog, PullRequest
33 from rhodecode.lib.user_log_filter import user_log_filter
37 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
34 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
38 from rhodecode.lib.base import BaseController, render
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 from rhodecode.lib.helpers import Page
37 from rhodecode.lib.helpers import Page
41
38
42
39
43 log = logging.getLogger(__name__)
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 class AdminController(BaseController):
43 class AdminController(BaseController):
128
44
@@ -139,7 +55,7
139 # FILTERING
55 # FILTERING
140 c.search_term = request.GET.get('filter')
56 c.search_term = request.GET.get('filter')
141 try:
57 try:
142 users_log = _journal_filter(users_log, c.search_term)
58 users_log = user_log_filter(users_log, c.search_term)
143 except Exception:
59 except Exception:
144 # we want this to crash for now
60 # we want this to crash for now
145 raise
61 raise
@@ -34,7 +34,7
34 from pylons import request, tmpl_context as c, response, url
34 from pylons import request, tmpl_context as c, response, url
35 from pylons.i18n.translation import _
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 from rhodecode.model.db import UserLog, UserFollowing, User, UserApiKeys
38 from rhodecode.model.db import UserLog, UserFollowing, User, UserApiKeys
39 from rhodecode.model.meta import Session
39 from rhodecode.model.meta import Session
40 import rhodecode.lib.helpers as h
40 import rhodecode.lib.helpers as h
@@ -89,7 +89,7
89 .options(joinedload(UserLog.repository))
89 .options(joinedload(UserLog.repository))
90 #filter
90 #filter
91 try:
91 try:
92 journal = _journal_filter(journal, c.search_term)
92 journal = user_log_filter(journal, c.search_term)
93 except Exception:
93 except Exception:
94 # we want this to crash for now
94 # we want this to crash for now
95 raise
95 raise
@@ -33,6 +33,7
33 from sqlalchemy.sql.expression import true, false
33 from sqlalchemy.sql.expression import true, false
34
34
35 from rhodecode import events
35 from rhodecode import events
36 from rhodecode.lib.user_log_filter import user_log_filter
36 from rhodecode.lib.utils2 import (
37 from rhodecode.lib.utils2 import (
37 safe_unicode, get_current_rhodecode_user, action_logger_generic,
38 safe_unicode, get_current_rhodecode_user, action_logger_generic,
38 AttributeDict, str2bool)
39 AttributeDict, str2bool)
@@ -40,7 +41,7
40 from rhodecode.model import BaseModel
41 from rhodecode.model import BaseModel
41 from rhodecode.model.auth_token import AuthTokenModel
42 from rhodecode.model.auth_token import AuthTokenModel
42 from rhodecode.model.db import (
43 from rhodecode.model.db import (
43 User, UserToPerm, UserEmailMap, UserIpMap)
44 or_, joinedload, User, UserToPerm, UserEmailMap, UserIpMap, UserLog)
44 from rhodecode.lib.exceptions import (
45 from rhodecode.lib.exceptions import (
45 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
46 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
46 UserOwnsUserGroupsException, NotAllowedToCreateUserError)
47 UserOwnsUserGroupsException, NotAllowedToCreateUserError)
@@ -847,3 +848,14
847 Session().commit()
848 Session().commit()
848
849
849 return
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
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>
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 <li class="${'active' if c.active=='emails' else ''}"><a href="${h.url('edit_user_emails', user_id=c.user.user_id)}">${_('Emails')}</a></li>
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 <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>
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
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=='groups' else ''}">
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>
42 <a href="${h.route_path('edit_user_groups_management', user_id=c.user.user_id)}">${_('User Groups Management')}</a>
43 </li>
44
45 </ul>
42 </ul>
46 </div>
43 </div>
47
44
Comments 0
You need to be logged in to leave comments. Login now