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( |
|
|
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 |
|
|
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 = |
|
|
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 |
|
|
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 = |
|
|
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