##// 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>
@@ -1,94 +1,99 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 from rhodecode.apps.admin.navigation import NavigationRegistry
23 23 from rhodecode.config.routing import ADMIN_PREFIX
24 24 from rhodecode.lib.utils2 import str2bool
25 25
26 26
27 27 def admin_routes(config):
28 28 """
29 29 Admin prefixed routes
30 30 """
31 31
32 32 config.add_route(
33 33 name='admin_settings_open_source',
34 34 pattern='/settings/open_source')
35 35 config.add_route(
36 36 name='admin_settings_vcs_svn_generate_cfg',
37 37 pattern='/settings/vcs/svn_generate_cfg')
38 38
39 39 config.add_route(
40 40 name='admin_settings_system',
41 41 pattern='/settings/system')
42 42 config.add_route(
43 43 name='admin_settings_system_update',
44 44 pattern='/settings/system/updates')
45 45
46 46 config.add_route(
47 47 name='admin_settings_sessions',
48 48 pattern='/settings/sessions')
49 49 config.add_route(
50 50 name='admin_settings_sessions_cleanup',
51 51 pattern='/settings/sessions/cleanup')
52 52
53 53 # users admin
54 54 config.add_route(
55 55 name='users',
56 56 pattern='/users')
57 57
58 58 config.add_route(
59 59 name='users_data',
60 60 pattern='/users_data')
61 61
62 62 # user auth tokens
63 63 config.add_route(
64 64 name='edit_user_auth_tokens',
65 65 pattern='/users/{user_id:\d+}/edit/auth_tokens')
66 66 config.add_route(
67 67 name='edit_user_auth_tokens_add',
68 68 pattern='/users/{user_id:\d+}/edit/auth_tokens/new')
69 69 config.add_route(
70 70 name='edit_user_auth_tokens_delete',
71 71 pattern='/users/{user_id:\d+}/edit/auth_tokens/delete')
72 72
73 73 # user groups management
74 74 config.add_route(
75 75 name='edit_user_groups_management',
76 76 pattern='/users/{user_id:\d+}/edit/groups_management')
77 77
78 78 config.add_route(
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()
85 90
86 91 # Create admin navigation registry and add it to the pyramid registry.
87 92 labs_active = str2bool(settings.get('labs_settings_active', False))
88 93 navigation_registry = NavigationRegistry(labs_active=labs_active)
89 94 config.registry.registerUtility(navigation_registry)
90 95
91 96 config.include(admin_routes, route_prefix=ADMIN_PREFIX)
92 97
93 98 # Scan module for configuration decorators.
94 99 config.scan()
@@ -1,285 +1,317 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 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
28 30 from rhodecode.lib.auth import (
29 31 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
30 32 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
37 40
38 41 log = logging.getLogger(__name__)
39 42
40 43
41 44 class AdminUsersView(BaseAppView):
42 45 ALLOW_SCOPED_TOKENS = False
43 46 """
44 47 This view has alternative version inside EE, if modified please take a look
45 48 in there as well.
46 49 """
47 50
48 51 def load_default_context(self):
49 52 c = self._get_local_tmpl_context()
50 53 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
51 54 self._register_global_c(c)
52 55 return c
53 56
54 57 def _redirect_for_default_user(self, username):
55 58 _ = self.request.translate
56 59 if username == User.DEFAULT_USER:
57 60 h.flash(_("You can't edit this user"), category='warning')
58 61 # TODO(marcink): redirect to 'users' admin panel once this
59 62 # is a pyramid view
60 63 raise HTTPFound('/')
61 64
62 65 def _extract_ordering(self, request):
63 66 column_index = safe_int(request.GET.get('order[0][column]'))
64 67 order_dir = request.GET.get(
65 68 'order[0][dir]', 'desc')
66 69 order_by = request.GET.get(
67 70 'columns[%s][data][sort]' % column_index, 'name_raw')
68 71
69 72 # translate datatable to DB columns
70 73 order_by = {
71 74 'first_name': 'name',
72 75 'last_name': 'lastname',
73 76 }.get(order_by) or order_by
74 77
75 78 search_q = request.GET.get('search[value]')
76 79 return search_q, order_by, order_dir
77 80
78 81 def _extract_chunk(self, request):
79 82 start = safe_int(request.GET.get('start'), 0)
80 83 length = safe_int(request.GET.get('length'), 25)
81 84 draw = safe_int(request.GET.get('draw'))
82 85 return draw, start, length
83 86
84 87 @HasPermissionAllDecorator('hg.admin')
85 88 @view_config(
86 89 route_name='users', request_method='GET',
87 90 renderer='rhodecode:templates/admin/users/users.mako')
88 91 def users_list(self):
89 92 c = self.load_default_context()
90 93 return self._get_template_context(c)
91 94
92 95 @HasPermissionAllDecorator('hg.admin')
93 96 @view_config(
94 97 # renderer defined below
95 98 route_name='users_data', request_method='GET', renderer='json',
96 99 xhr=True)
97 100 def users_list_data(self):
98 101 draw, start, limit = self._extract_chunk(self.request)
99 102 search_q, order_by, order_dir = self._extract_ordering(self.request)
100 103
101 104 _render = PartialRenderer('data_table/_dt_elements.mako')
102 105
103 106 def user_actions(user_id, username):
104 107 return _render("user_actions", user_id, username)
105 108
106 109 users_data_total_count = User.query()\
107 110 .filter(User.username != User.DEFAULT_USER) \
108 111 .count()
109 112
110 113 # json generate
111 114 base_q = User.query().filter(User.username != User.DEFAULT_USER)
112 115
113 116 if search_q:
114 117 like_expression = u'%{}%'.format(safe_unicode(search_q))
115 118 base_q = base_q.filter(or_(
116 119 User.username.ilike(like_expression),
117 120 User._email.ilike(like_expression),
118 121 User.name.ilike(like_expression),
119 122 User.lastname.ilike(like_expression),
120 123 ))
121 124
122 125 users_data_total_filtered_count = base_q.count()
123 126
124 127 sort_col = getattr(User, order_by, None)
125 128 if sort_col and order_dir == 'asc':
126 129 base_q = base_q.order_by(sort_col.asc().nullslast())
127 130 elif sort_col:
128 131 base_q = base_q.order_by(sort_col.desc().nullslast())
129 132
130 133 base_q = base_q.offset(start).limit(limit)
131 134 users_list = base_q.all()
132 135
133 136 users_data = []
134 137 for user in users_list:
135 138 users_data.append({
136 139 "username": h.gravatar_with_user(user.username),
137 140 "email": user.email,
138 141 "first_name": h.escape(user.name),
139 142 "last_name": h.escape(user.lastname),
140 143 "last_login": h.format_date(user.last_login),
141 144 "last_activity": h.format_date(user.last_activity),
142 145 "active": h.bool2icon(user.active),
143 146 "active_raw": user.active,
144 147 "admin": h.bool2icon(user.admin),
145 148 "extern_type": user.extern_type,
146 149 "extern_name": user.extern_name,
147 150 "action": user_actions(user.user_id, user.username),
148 151 })
149 152
150 153 data = ({
151 154 'draw': draw,
152 155 'data': users_data,
153 156 'recordsTotal': users_data_total_count,
154 157 'recordsFiltered': users_data_total_filtered_count,
155 158 })
156 159
157 160 return data
158 161
159 162 @LoginRequired()
160 163 @HasPermissionAllDecorator('hg.admin')
161 164 @view_config(
162 165 route_name='edit_user_auth_tokens', request_method='GET',
163 166 renderer='rhodecode:templates/admin/users/user_edit.mako')
164 167 def auth_tokens(self):
165 168 _ = self.request.translate
166 169 c = self.load_default_context()
167 170
168 171 user_id = self.request.matchdict.get('user_id')
169 172 c.user = User.get_or_404(user_id, pyramid_exc=True)
170 173 self._redirect_for_default_user(c.user.username)
171 174
172 175 c.active = 'auth_tokens'
173 176
174 177 c.lifetime_values = [
175 178 (str(-1), _('forever')),
176 179 (str(5), _('5 minutes')),
177 180 (str(60), _('1 hour')),
178 181 (str(60 * 24), _('1 day')),
179 182 (str(60 * 24 * 30), _('1 month')),
180 183 ]
181 184 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
182 185 c.role_values = [
183 186 (x, AuthTokenModel.cls._get_role_name(x))
184 187 for x in AuthTokenModel.cls.ROLES]
185 188 c.role_options = [(c.role_values, _("Role"))]
186 189 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
187 190 c.user.user_id, show_expired=True)
188 191 return self._get_template_context(c)
189 192
190 193 def maybe_attach_token_scope(self, token):
191 194 # implemented in EE edition
192 195 pass
193 196
194 197 @LoginRequired()
195 198 @HasPermissionAllDecorator('hg.admin')
196 199 @CSRFRequired()
197 200 @view_config(
198 201 route_name='edit_user_auth_tokens_add', request_method='POST')
199 202 def auth_tokens_add(self):
200 203 _ = self.request.translate
201 204 c = self.load_default_context()
202 205
203 206 user_id = self.request.matchdict.get('user_id')
204 207 c.user = User.get_or_404(user_id, pyramid_exc=True)
205 208 self._redirect_for_default_user(c.user.username)
206 209
207 210 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
208 211 description = self.request.POST.get('description')
209 212 role = self.request.POST.get('role')
210 213
211 214 token = AuthTokenModel().create(
212 215 c.user.user_id, description, lifetime, role)
213 216 self.maybe_attach_token_scope(token)
214 217 Session().commit()
215 218
216 219 h.flash(_("Auth token successfully created"), category='success')
217 220 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
218 221
219 222 @LoginRequired()
220 223 @HasPermissionAllDecorator('hg.admin')
221 224 @CSRFRequired()
222 225 @view_config(
223 226 route_name='edit_user_auth_tokens_delete', request_method='POST')
224 227 def auth_tokens_delete(self):
225 228 _ = self.request.translate
226 229 c = self.load_default_context()
227 230
228 231 user_id = self.request.matchdict.get('user_id')
229 232 c.user = User.get_or_404(user_id, pyramid_exc=True)
230 233 self._redirect_for_default_user(c.user.username)
231 234
232 235 del_auth_token = self.request.POST.get('del_auth_token')
233 236
234 237 if del_auth_token:
235 238 AuthTokenModel().delete(del_auth_token, c.user.user_id)
236 239 Session().commit()
237 240 h.flash(_("Auth token successfully deleted"), category='success')
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(
245 247 route_name='edit_user_groups_management', request_method='GET',
246 248 renderer='rhodecode:templates/admin/users/user_edit.mako')
247 249 def groups_management(self):
248 250 c = self.load_default_context()
249 251
250 252 user_id = self.request.matchdict.get('user_id')
251 253 c.user = User.get_or_404(user_id, pyramid_exc=True)
252 254 c.data = c.user.group_member
253 255 self._redirect_for_default_user(c.user.username)
254 256 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group) for group in c.user.group_member]
255 257 c.groups = json.dumps(groups)
256 258 c.active = 'groups'
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(
264 265 route_name='edit_user_groups_management_updates', request_method='POST')
265 266 def groups_management_updates(self):
266 267 _ = self.request.translate
267 268 c = self.load_default_context()
268 269
269 270 user_id = self.request.matchdict.get('user_id')
270 271 c.user = User.get_or_404(user_id, pyramid_exc=True)
271 272 self._redirect_for_default_user(c.user.username)
272 273
273 274 users_groups = set(self.request.POST.getall('users_group_id'))
274 275 users_groups_model = []
275 276
276 277 for ugid in users_groups:
277 278 users_groups_model.append(UserGroupModel().get_group(safe_int(ugid)))
278 279 user_group_model = UserGroupModel()
279 280 user_group_model.change_groups(c.user, users_groups_model)
280 281
281 282 Session().commit()
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
@@ -1,173 +1,89 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Controller for Admin panel of RhodeCode Enterprise
23 23 """
24 24
25 25
26 26 import logging
27 27
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
129 45 @LoginRequired()
130 46 def __before__(self):
131 47 super(AdminController, self).__before__()
132 48
133 49 @HasPermissionAllDecorator('hg.admin')
134 50 def index(self):
135 51 users_log = UserLog.query()\
136 52 .options(joinedload(UserLog.user))\
137 53 .options(joinedload(UserLog.repository))
138 54
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
146 62
147 63 users_log = users_log.order_by(UserLog.action_date.desc())
148 64
149 65 p = safe_int(request.GET.get('page', 1), 1)
150 66
151 67 def url_generator(**kw):
152 68 return url.current(filter=c.search_term, **kw)
153 69
154 70 c.users_log = Page(users_log, page=p, items_per_page=10,
155 71 url=url_generator)
156 72 c.log_data = render('admin/admin_log.mako')
157 73
158 74 if request.is_xhr:
159 75 return c.log_data
160 76 return render('admin/admin.mako')
161 77
162 78 # global redirect doesn't need permissions
163 79 def pull_requests(self, pull_request_id):
164 80 """
165 81 Global redirect for Pull Requests
166 82
167 83 :param pull_request_id: id of pull requests in the system
168 84 """
169 85 pull_request = PullRequest.get_or_404(pull_request_id)
170 86 repo_name = pull_request.target_repo.repo_name
171 87 return redirect(url(
172 88 'pullrequest_show', repo_name=repo_name,
173 89 pull_request_id=pull_request_id))
@@ -1,306 +1,306 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Journal / user event log controller for rhodecode
23 23 """
24 24
25 25 import logging
26 26 from itertools import groupby
27 27
28 28 from sqlalchemy import or_
29 29 from sqlalchemy.orm import joinedload
30 30
31 31 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
32 32
33 33 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
41 41 from rhodecode.lib.helpers import Page
42 42 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
43 43 from rhodecode.lib.base import BaseController, render
44 44 from rhodecode.lib.utils2 import safe_int, AttributeDict
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 class JournalController(BaseController):
50 50
51 51 def __before__(self):
52 52 super(JournalController, self).__before__()
53 53 self.language = 'en-us'
54 54 self.ttl = "5"
55 55 self.feed_nr = 20
56 56 c.search_term = request.GET.get('filter')
57 57
58 58 def _get_daily_aggregate(self, journal):
59 59 groups = []
60 60 for k, g in groupby(journal, lambda x: x.action_as_day):
61 61 user_group = []
62 62 #groupby username if it's a present value, else fallback to journal username
63 63 for _, g2 in groupby(list(g), lambda x: x.user.username if x.user else x.username):
64 64 l = list(g2)
65 65 user_group.append((l[0].user, l))
66 66
67 67 groups.append((k, user_group,))
68 68
69 69 return groups
70 70
71 71 def _get_journal_data(self, following_repos):
72 72 repo_ids = [x.follows_repository.repo_id for x in following_repos
73 73 if x.follows_repository is not None]
74 74 user_ids = [x.follows_user.user_id for x in following_repos
75 75 if x.follows_user is not None]
76 76
77 77 filtering_criterion = None
78 78
79 79 if repo_ids and user_ids:
80 80 filtering_criterion = or_(UserLog.repository_id.in_(repo_ids),
81 81 UserLog.user_id.in_(user_ids))
82 82 if repo_ids and not user_ids:
83 83 filtering_criterion = UserLog.repository_id.in_(repo_ids)
84 84 if not repo_ids and user_ids:
85 85 filtering_criterion = UserLog.user_id.in_(user_ids)
86 86 if filtering_criterion is not None:
87 87 journal = self.sa.query(UserLog)\
88 88 .options(joinedload(UserLog.user))\
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
96 96 journal = journal.filter(filtering_criterion)\
97 97 .order_by(UserLog.action_date.desc())
98 98 else:
99 99 journal = []
100 100
101 101 return journal
102 102
103 103 def _atom_feed(self, repos, public=True):
104 104 journal = self._get_journal_data(repos)
105 105 if public:
106 106 _link = url('public_journal_atom', qualified=True)
107 107 _desc = '%s %s %s' % (c.rhodecode_name, _('public journal'),
108 108 'atom feed')
109 109 else:
110 110 _link = url('journal_atom', qualified=True)
111 111 _desc = '%s %s %s' % (c.rhodecode_name, _('journal'), 'atom feed')
112 112
113 113 feed = Atom1Feed(title=_desc,
114 114 link=_link,
115 115 description=_desc,
116 116 language=self.language,
117 117 ttl=self.ttl)
118 118
119 119 for entry in journal[:self.feed_nr]:
120 120 user = entry.user
121 121 if user is None:
122 122 #fix deleted users
123 123 user = AttributeDict({'short_contact': entry.username,
124 124 'email': '',
125 125 'full_contact': ''})
126 126 action, action_extra, ico = h.action_parser(entry, feed=True)
127 127 title = "%s - %s %s" % (user.short_contact, action(),
128 128 entry.repository.repo_name)
129 129 desc = action_extra()
130 130 _url = None
131 131 if entry.repository is not None:
132 132 _url = url('changelog_home',
133 133 repo_name=entry.repository.repo_name,
134 134 qualified=True)
135 135
136 136 feed.add_item(title=title,
137 137 pubdate=entry.action_date,
138 138 link=_url or url('', qualified=True),
139 139 author_email=user.email,
140 140 author_name=user.full_contact,
141 141 description=desc)
142 142
143 143 response.content_type = feed.mime_type
144 144 return feed.writeString('utf-8')
145 145
146 146 def _rss_feed(self, repos, public=True):
147 147 journal = self._get_journal_data(repos)
148 148 if public:
149 149 _link = url('public_journal_atom', qualified=True)
150 150 _desc = '%s %s %s' % (c.rhodecode_name, _('public journal'),
151 151 'rss feed')
152 152 else:
153 153 _link = url('journal_atom', qualified=True)
154 154 _desc = '%s %s %s' % (c.rhodecode_name, _('journal'), 'rss feed')
155 155
156 156 feed = Rss201rev2Feed(title=_desc,
157 157 link=_link,
158 158 description=_desc,
159 159 language=self.language,
160 160 ttl=self.ttl)
161 161
162 162 for entry in journal[:self.feed_nr]:
163 163 user = entry.user
164 164 if user is None:
165 165 #fix deleted users
166 166 user = AttributeDict({'short_contact': entry.username,
167 167 'email': '',
168 168 'full_contact': ''})
169 169 action, action_extra, ico = h.action_parser(entry, feed=True)
170 170 title = "%s - %s %s" % (user.short_contact, action(),
171 171 entry.repository.repo_name)
172 172 desc = action_extra()
173 173 _url = None
174 174 if entry.repository is not None:
175 175 _url = url('changelog_home',
176 176 repo_name=entry.repository.repo_name,
177 177 qualified=True)
178 178
179 179 feed.add_item(title=title,
180 180 pubdate=entry.action_date,
181 181 link=_url or url('', qualified=True),
182 182 author_email=user.email,
183 183 author_name=user.full_contact,
184 184 description=desc)
185 185
186 186 response.content_type = feed.mime_type
187 187 return feed.writeString('utf-8')
188 188
189 189 @LoginRequired()
190 190 @NotAnonymous()
191 191 def index(self):
192 192 # Return a rendered template
193 193 p = safe_int(request.GET.get('page', 1), 1)
194 194 c.user = User.get(c.rhodecode_user.user_id)
195 195 following = self.sa.query(UserFollowing)\
196 196 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
197 197 .options(joinedload(UserFollowing.follows_repository))\
198 198 .all()
199 199
200 200 journal = self._get_journal_data(following)
201 201
202 202 def url_generator(**kw):
203 203 return url.current(filter=c.search_term, **kw)
204 204
205 205 c.journal_pager = Page(journal, page=p, items_per_page=20, url=url_generator)
206 206 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
207 207
208 208 c.journal_data = render('journal/journal_data.mako')
209 209 if request.is_xhr:
210 210 return c.journal_data
211 211
212 212 return render('journal/journal.mako')
213 213
214 214 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
215 215 @NotAnonymous()
216 216 def journal_atom(self):
217 217 """
218 218 Produce an atom-1.0 feed via feedgenerator module
219 219 """
220 220 following = self.sa.query(UserFollowing)\
221 221 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
222 222 .options(joinedload(UserFollowing.follows_repository))\
223 223 .all()
224 224 return self._atom_feed(following, public=False)
225 225
226 226 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
227 227 @NotAnonymous()
228 228 def journal_rss(self):
229 229 """
230 230 Produce an rss feed via feedgenerator module
231 231 """
232 232 following = self.sa.query(UserFollowing)\
233 233 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
234 234 .options(joinedload(UserFollowing.follows_repository))\
235 235 .all()
236 236 return self._rss_feed(following, public=False)
237 237
238 238 @CSRFRequired()
239 239 @LoginRequired()
240 240 @NotAnonymous()
241 241 def toggle_following(self):
242 242 user_id = request.POST.get('follows_user_id')
243 243 if user_id:
244 244 try:
245 245 self.scm_model.toggle_following_user(
246 246 user_id, c.rhodecode_user.user_id)
247 247 Session().commit()
248 248 return 'ok'
249 249 except Exception:
250 250 raise HTTPBadRequest()
251 251
252 252 repo_id = request.POST.get('follows_repo_id')
253 253 if repo_id:
254 254 try:
255 255 self.scm_model.toggle_following_repo(
256 256 repo_id, c.rhodecode_user.user_id)
257 257 Session().commit()
258 258 return 'ok'
259 259 except Exception:
260 260 raise HTTPBadRequest()
261 261
262 262
263 263 @LoginRequired()
264 264 def public_journal(self):
265 265 # Return a rendered template
266 266 p = safe_int(request.GET.get('page', 1), 1)
267 267
268 268 c.following = self.sa.query(UserFollowing)\
269 269 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
270 270 .options(joinedload(UserFollowing.follows_repository))\
271 271 .all()
272 272
273 273 journal = self._get_journal_data(c.following)
274 274
275 275 c.journal_pager = Page(journal, page=p, items_per_page=20)
276 276
277 277 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
278 278
279 279 c.journal_data = render('journal/journal_data.mako')
280 280 if request.is_xhr:
281 281 return c.journal_data
282 282 return render('journal/public_journal.mako')
283 283
284 284 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
285 285 def public_journal_atom(self):
286 286 """
287 287 Produce an atom-1.0 feed via feedgenerator module
288 288 """
289 289 c.following = self.sa.query(UserFollowing)\
290 290 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
291 291 .options(joinedload(UserFollowing.follows_repository))\
292 292 .all()
293 293
294 294 return self._atom_feed(c.following)
295 295
296 296 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
297 297 def public_journal_rss(self):
298 298 """
299 299 Produce an rss2 feed via feedgenerator module
300 300 """
301 301 c.following = self.sa.query(UserFollowing)\
302 302 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
303 303 .options(joinedload(UserFollowing.follows_repository))\
304 304 .all()
305 305
306 306 return self._rss_feed(c.following)
@@ -1,849 +1,861 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 users model for RhodeCode
23 23 """
24 24
25 25 import logging
26 26 import traceback
27 27
28 28 import datetime
29 29 from pylons.i18n.translation import _
30 30
31 31 import ipaddress
32 32 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)
39 40 from rhodecode.lib.caching_query import FromCache
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)
47 48 from rhodecode.model.meta import Session
48 49 from rhodecode.model.repo_group import RepoGroupModel
49 50
50 51
51 52 log = logging.getLogger(__name__)
52 53
53 54
54 55 class UserModel(BaseModel):
55 56 cls = User
56 57
57 58 def get(self, user_id, cache=False):
58 59 user = self.sa.query(User)
59 60 if cache:
60 61 user = user.options(FromCache("sql_cache_short",
61 62 "get_user_%s" % user_id))
62 63 return user.get(user_id)
63 64
64 65 def get_user(self, user):
65 66 return self._get_user(user)
66 67
67 68 def get_by_username(self, username, cache=False, case_insensitive=False):
68 69
69 70 if case_insensitive:
70 71 user = self.sa.query(User).filter(User.username.ilike(username))
71 72 else:
72 73 user = self.sa.query(User)\
73 74 .filter(User.username == username)
74 75 if cache:
75 76 user = user.options(FromCache("sql_cache_short",
76 77 "get_user_%s" % username))
77 78 return user.scalar()
78 79
79 80 def get_by_email(self, email, cache=False, case_insensitive=False):
80 81 return User.get_by_email(email, case_insensitive, cache)
81 82
82 83 def get_by_auth_token(self, auth_token, cache=False):
83 84 return User.get_by_auth_token(auth_token, cache)
84 85
85 86 def get_active_user_count(self, cache=False):
86 87 return User.query().filter(
87 88 User.active == True).filter(
88 89 User.username != User.DEFAULT_USER).count()
89 90
90 91 def create(self, form_data, cur_user=None):
91 92 if not cur_user:
92 93 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
93 94
94 95 user_data = {
95 96 'username': form_data['username'],
96 97 'password': form_data['password'],
97 98 'email': form_data['email'],
98 99 'firstname': form_data['firstname'],
99 100 'lastname': form_data['lastname'],
100 101 'active': form_data['active'],
101 102 'extern_type': form_data['extern_type'],
102 103 'extern_name': form_data['extern_name'],
103 104 'admin': False,
104 105 'cur_user': cur_user
105 106 }
106 107
107 108 if 'create_repo_group' in form_data:
108 109 user_data['create_repo_group'] = str2bool(
109 110 form_data.get('create_repo_group'))
110 111
111 112 try:
112 113 if form_data.get('password_change'):
113 114 user_data['force_password_change'] = True
114 115 return UserModel().create_or_update(**user_data)
115 116 except Exception:
116 117 log.error(traceback.format_exc())
117 118 raise
118 119
119 120 def update_user(self, user, skip_attrs=None, **kwargs):
120 121 from rhodecode.lib.auth import get_crypt_password
121 122
122 123 user = self._get_user(user)
123 124 if user.username == User.DEFAULT_USER:
124 125 raise DefaultUserException(
125 126 _("You can't Edit this user since it's"
126 127 " crucial for entire application"))
127 128
128 129 # first store only defaults
129 130 user_attrs = {
130 131 'updating_user_id': user.user_id,
131 132 'username': user.username,
132 133 'password': user.password,
133 134 'email': user.email,
134 135 'firstname': user.name,
135 136 'lastname': user.lastname,
136 137 'active': user.active,
137 138 'admin': user.admin,
138 139 'extern_name': user.extern_name,
139 140 'extern_type': user.extern_type,
140 141 'language': user.user_data.get('language')
141 142 }
142 143
143 144 # in case there's new_password, that comes from form, use it to
144 145 # store password
145 146 if kwargs.get('new_password'):
146 147 kwargs['password'] = kwargs['new_password']
147 148
148 149 # cleanups, my_account password change form
149 150 kwargs.pop('current_password', None)
150 151 kwargs.pop('new_password', None)
151 152
152 153 # cleanups, user edit password change form
153 154 kwargs.pop('password_confirmation', None)
154 155 kwargs.pop('password_change', None)
155 156
156 157 # create repo group on user creation
157 158 kwargs.pop('create_repo_group', None)
158 159
159 160 # legacy forms send name, which is the firstname
160 161 firstname = kwargs.pop('name', None)
161 162 if firstname:
162 163 kwargs['firstname'] = firstname
163 164
164 165 for k, v in kwargs.items():
165 166 # skip if we don't want to update this
166 167 if skip_attrs and k in skip_attrs:
167 168 continue
168 169
169 170 user_attrs[k] = v
170 171
171 172 try:
172 173 return self.create_or_update(**user_attrs)
173 174 except Exception:
174 175 log.error(traceback.format_exc())
175 176 raise
176 177
177 178 def create_or_update(
178 179 self, username, password, email, firstname='', lastname='',
179 180 active=True, admin=False, extern_type=None, extern_name=None,
180 181 cur_user=None, plugin=None, force_password_change=False,
181 182 allow_to_create_user=True, create_repo_group=None,
182 183 updating_user_id=None, language=None, strict_creation_check=True):
183 184 """
184 185 Creates a new instance if not found, or updates current one
185 186
186 187 :param username:
187 188 :param password:
188 189 :param email:
189 190 :param firstname:
190 191 :param lastname:
191 192 :param active:
192 193 :param admin:
193 194 :param extern_type:
194 195 :param extern_name:
195 196 :param cur_user:
196 197 :param plugin: optional plugin this method was called from
197 198 :param force_password_change: toggles new or existing user flag
198 199 for password change
199 200 :param allow_to_create_user: Defines if the method can actually create
200 201 new users
201 202 :param create_repo_group: Defines if the method should also
202 203 create an repo group with user name, and owner
203 204 :param updating_user_id: if we set it up this is the user we want to
204 205 update this allows to editing username.
205 206 :param language: language of user from interface.
206 207
207 208 :returns: new User object with injected `is_new_user` attribute.
208 209 """
209 210 if not cur_user:
210 211 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
211 212
212 213 from rhodecode.lib.auth import (
213 214 get_crypt_password, check_password, generate_auth_token)
214 215 from rhodecode.lib.hooks_base import (
215 216 log_create_user, check_allowed_create_user)
216 217
217 218 def _password_change(new_user, password):
218 219 # empty password
219 220 if not new_user.password:
220 221 return False
221 222
222 223 # password check is only needed for RhodeCode internal auth calls
223 224 # in case it's a plugin we don't care
224 225 if not plugin:
225 226
226 227 # first check if we gave crypted password back, and if it
227 228 # matches it's not password change
228 229 if new_user.password == password:
229 230 return False
230 231
231 232 password_match = check_password(password, new_user.password)
232 233 if not password_match:
233 234 return True
234 235
235 236 return False
236 237
237 238 # read settings on default personal repo group creation
238 239 if create_repo_group is None:
239 240 default_create_repo_group = RepoGroupModel()\
240 241 .get_default_create_personal_repo_group()
241 242 create_repo_group = default_create_repo_group
242 243
243 244 user_data = {
244 245 'username': username,
245 246 'password': password,
246 247 'email': email,
247 248 'firstname': firstname,
248 249 'lastname': lastname,
249 250 'active': active,
250 251 'admin': admin
251 252 }
252 253
253 254 if updating_user_id:
254 255 log.debug('Checking for existing account in RhodeCode '
255 256 'database with user_id `%s` ' % (updating_user_id,))
256 257 user = User.get(updating_user_id)
257 258 else:
258 259 log.debug('Checking for existing account in RhodeCode '
259 260 'database with username `%s` ' % (username,))
260 261 user = User.get_by_username(username, case_insensitive=True)
261 262
262 263 if user is None:
263 264 # we check internal flag if this method is actually allowed to
264 265 # create new user
265 266 if not allow_to_create_user:
266 267 msg = ('Method wants to create new user, but it is not '
267 268 'allowed to do so')
268 269 log.warning(msg)
269 270 raise NotAllowedToCreateUserError(msg)
270 271
271 272 log.debug('Creating new user %s', username)
272 273
273 274 # only if we create user that is active
274 275 new_active_user = active
275 276 if new_active_user and strict_creation_check:
276 277 # raises UserCreationError if it's not allowed for any reason to
277 278 # create new active user, this also executes pre-create hooks
278 279 check_allowed_create_user(user_data, cur_user, strict_check=True)
279 280 events.trigger(events.UserPreCreate(user_data))
280 281 new_user = User()
281 282 edit = False
282 283 else:
283 284 log.debug('updating user %s', username)
284 285 events.trigger(events.UserPreUpdate(user, user_data))
285 286 new_user = user
286 287 edit = True
287 288
288 289 # we're not allowed to edit default user
289 290 if user.username == User.DEFAULT_USER:
290 291 raise DefaultUserException(
291 292 _("You can't edit this user (`%(username)s`) since it's "
292 293 "crucial for entire application") % {'username': user.username})
293 294
294 295 # inject special attribute that will tell us if User is new or old
295 296 new_user.is_new_user = not edit
296 297 # for users that didn's specify auth type, we use RhodeCode built in
297 298 from rhodecode.authentication.plugins import auth_rhodecode
298 299 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.name
299 300 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.name
300 301
301 302 try:
302 303 new_user.username = username
303 304 new_user.admin = admin
304 305 new_user.email = email
305 306 new_user.active = active
306 307 new_user.extern_name = safe_unicode(extern_name)
307 308 new_user.extern_type = safe_unicode(extern_type)
308 309 new_user.name = firstname
309 310 new_user.lastname = lastname
310 311
311 312 # set password only if creating an user or password is changed
312 313 if not edit or _password_change(new_user, password):
313 314 reason = 'new password' if edit else 'new user'
314 315 log.debug('Updating password reason=>%s', reason)
315 316 new_user.password = get_crypt_password(password) if password else None
316 317
317 318 if force_password_change:
318 319 new_user.update_userdata(force_password_change=True)
319 320 if language:
320 321 new_user.update_userdata(language=language)
321 322 new_user.update_userdata(notification_status=True)
322 323
323 324 self.sa.add(new_user)
324 325
325 326 if not edit and create_repo_group:
326 327 RepoGroupModel().create_personal_repo_group(
327 328 new_user, commit_early=False)
328 329
329 330 if not edit:
330 331 # add the RSS token
331 332 AuthTokenModel().create(username,
332 333 description='Generated feed token',
333 334 role=AuthTokenModel.cls.ROLE_FEED)
334 335 log_create_user(created_by=cur_user, **new_user.get_dict())
335 336 events.trigger(events.UserPostCreate(user_data))
336 337 return new_user
337 338 except (DatabaseError,):
338 339 log.error(traceback.format_exc())
339 340 raise
340 341
341 342 def create_registration(self, form_data):
342 343 from rhodecode.model.notification import NotificationModel
343 344 from rhodecode.model.notification import EmailNotificationModel
344 345
345 346 try:
346 347 form_data['admin'] = False
347 348 form_data['extern_name'] = 'rhodecode'
348 349 form_data['extern_type'] = 'rhodecode'
349 350 new_user = self.create(form_data)
350 351
351 352 self.sa.add(new_user)
352 353 self.sa.flush()
353 354
354 355 user_data = new_user.get_dict()
355 356 kwargs = {
356 357 # use SQLALCHEMY safe dump of user data
357 358 'user': AttributeDict(user_data),
358 359 'date': datetime.datetime.now()
359 360 }
360 361 notification_type = EmailNotificationModel.TYPE_REGISTRATION
361 362 # pre-generate the subject for notification itself
362 363 (subject,
363 364 _h, _e, # we don't care about those
364 365 body_plaintext) = EmailNotificationModel().render_email(
365 366 notification_type, **kwargs)
366 367
367 368 # create notification objects, and emails
368 369 NotificationModel().create(
369 370 created_by=new_user,
370 371 notification_subject=subject,
371 372 notification_body=body_plaintext,
372 373 notification_type=notification_type,
373 374 recipients=None, # all admins
374 375 email_kwargs=kwargs,
375 376 )
376 377
377 378 return new_user
378 379 except Exception:
379 380 log.error(traceback.format_exc())
380 381 raise
381 382
382 383 def _handle_user_repos(self, username, repositories, handle_mode=None):
383 384 _superadmin = self.cls.get_first_super_admin()
384 385 left_overs = True
385 386
386 387 from rhodecode.model.repo import RepoModel
387 388
388 389 if handle_mode == 'detach':
389 390 for obj in repositories:
390 391 obj.user = _superadmin
391 392 # set description we know why we super admin now owns
392 393 # additional repositories that were orphaned !
393 394 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
394 395 self.sa.add(obj)
395 396 left_overs = False
396 397 elif handle_mode == 'delete':
397 398 for obj in repositories:
398 399 RepoModel().delete(obj, forks='detach')
399 400 left_overs = False
400 401
401 402 # if nothing is done we have left overs left
402 403 return left_overs
403 404
404 405 def _handle_user_repo_groups(self, username, repository_groups,
405 406 handle_mode=None):
406 407 _superadmin = self.cls.get_first_super_admin()
407 408 left_overs = True
408 409
409 410 from rhodecode.model.repo_group import RepoGroupModel
410 411
411 412 if handle_mode == 'detach':
412 413 for r in repository_groups:
413 414 r.user = _superadmin
414 415 # set description we know why we super admin now owns
415 416 # additional repositories that were orphaned !
416 417 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
417 418 self.sa.add(r)
418 419 left_overs = False
419 420 elif handle_mode == 'delete':
420 421 for r in repository_groups:
421 422 RepoGroupModel().delete(r)
422 423 left_overs = False
423 424
424 425 # if nothing is done we have left overs left
425 426 return left_overs
426 427
427 428 def _handle_user_user_groups(self, username, user_groups, handle_mode=None):
428 429 _superadmin = self.cls.get_first_super_admin()
429 430 left_overs = True
430 431
431 432 from rhodecode.model.user_group import UserGroupModel
432 433
433 434 if handle_mode == 'detach':
434 435 for r in user_groups:
435 436 for user_user_group_to_perm in r.user_user_group_to_perm:
436 437 if user_user_group_to_perm.user.username == username:
437 438 user_user_group_to_perm.user = _superadmin
438 439 r.user = _superadmin
439 440 # set description we know why we super admin now owns
440 441 # additional repositories that were orphaned !
441 442 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
442 443 self.sa.add(r)
443 444 left_overs = False
444 445 elif handle_mode == 'delete':
445 446 for r in user_groups:
446 447 UserGroupModel().delete(r)
447 448 left_overs = False
448 449
449 450 # if nothing is done we have left overs left
450 451 return left_overs
451 452
452 453 def delete(self, user, cur_user=None, handle_repos=None,
453 454 handle_repo_groups=None, handle_user_groups=None):
454 455 if not cur_user:
455 456 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
456 457 user = self._get_user(user)
457 458
458 459 try:
459 460 if user.username == User.DEFAULT_USER:
460 461 raise DefaultUserException(
461 462 _(u"You can't remove this user since it's"
462 463 u" crucial for entire application"))
463 464
464 465 left_overs = self._handle_user_repos(
465 466 user.username, user.repositories, handle_repos)
466 467 if left_overs and user.repositories:
467 468 repos = [x.repo_name for x in user.repositories]
468 469 raise UserOwnsReposException(
469 470 _(u'user "%s" still owns %s repositories and cannot be '
470 471 u'removed. Switch owners or remove those repositories:%s')
471 472 % (user.username, len(repos), ', '.join(repos)))
472 473
473 474 left_overs = self._handle_user_repo_groups(
474 475 user.username, user.repository_groups, handle_repo_groups)
475 476 if left_overs and user.repository_groups:
476 477 repo_groups = [x.group_name for x in user.repository_groups]
477 478 raise UserOwnsRepoGroupsException(
478 479 _(u'user "%s" still owns %s repository groups and cannot be '
479 480 u'removed. Switch owners or remove those repository groups:%s')
480 481 % (user.username, len(repo_groups), ', '.join(repo_groups)))
481 482
482 483 left_overs = self._handle_user_user_groups(
483 484 user.username, user.user_groups, handle_user_groups)
484 485 if left_overs and user.user_groups:
485 486 user_groups = [x.users_group_name for x in user.user_groups]
486 487 raise UserOwnsUserGroupsException(
487 488 _(u'user "%s" still owns %s user groups and cannot be '
488 489 u'removed. Switch owners or remove those user groups:%s')
489 490 % (user.username, len(user_groups), ', '.join(user_groups)))
490 491
491 492 # we might change the user data with detach/delete, make sure
492 493 # the object is marked as expired before actually deleting !
493 494 self.sa.expire(user)
494 495 self.sa.delete(user)
495 496 from rhodecode.lib.hooks_base import log_delete_user
496 497 log_delete_user(deleted_by=cur_user, **user.get_dict())
497 498 except Exception:
498 499 log.error(traceback.format_exc())
499 500 raise
500 501
501 502 def reset_password_link(self, data, pwd_reset_url):
502 503 from rhodecode.lib.celerylib import tasks, run_task
503 504 from rhodecode.model.notification import EmailNotificationModel
504 505 user_email = data['email']
505 506 try:
506 507 user = User.get_by_email(user_email)
507 508 if user:
508 509 log.debug('password reset user found %s', user)
509 510
510 511 email_kwargs = {
511 512 'password_reset_url': pwd_reset_url,
512 513 'user': user,
513 514 'email': user_email,
514 515 'date': datetime.datetime.now()
515 516 }
516 517
517 518 (subject, headers, email_body,
518 519 email_body_plaintext) = EmailNotificationModel().render_email(
519 520 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
520 521
521 522 recipients = [user_email]
522 523
523 524 action_logger_generic(
524 525 'sending password reset email to user: {}'.format(
525 526 user), namespace='security.password_reset')
526 527
527 528 run_task(tasks.send_email, recipients, subject,
528 529 email_body_plaintext, email_body)
529 530
530 531 else:
531 532 log.debug("password reset email %s not found", user_email)
532 533 except Exception:
533 534 log.error(traceback.format_exc())
534 535 return False
535 536
536 537 return True
537 538
538 539 def reset_password(self, data):
539 540 from rhodecode.lib.celerylib import tasks, run_task
540 541 from rhodecode.model.notification import EmailNotificationModel
541 542 from rhodecode.lib import auth
542 543 user_email = data['email']
543 544 pre_db = True
544 545 try:
545 546 user = User.get_by_email(user_email)
546 547 new_passwd = auth.PasswordGenerator().gen_password(
547 548 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
548 549 if user:
549 550 user.password = auth.get_crypt_password(new_passwd)
550 551 # also force this user to reset his password !
551 552 user.update_userdata(force_password_change=True)
552 553
553 554 Session().add(user)
554 555
555 556 # now delete the token in question
556 557 UserApiKeys = AuthTokenModel.cls
557 558 UserApiKeys().query().filter(
558 559 UserApiKeys.api_key == data['token']).delete()
559 560
560 561 Session().commit()
561 562 log.info('successfully reset password for `%s`', user_email)
562 563
563 564 if new_passwd is None:
564 565 raise Exception('unable to generate new password')
565 566
566 567 pre_db = False
567 568
568 569 email_kwargs = {
569 570 'new_password': new_passwd,
570 571 'user': user,
571 572 'email': user_email,
572 573 'date': datetime.datetime.now()
573 574 }
574 575
575 576 (subject, headers, email_body,
576 577 email_body_plaintext) = EmailNotificationModel().render_email(
577 578 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
578 579 **email_kwargs)
579 580
580 581 recipients = [user_email]
581 582
582 583 action_logger_generic(
583 584 'sent new password to user: {} with email: {}'.format(
584 585 user, user_email), namespace='security.password_reset')
585 586
586 587 run_task(tasks.send_email, recipients, subject,
587 588 email_body_plaintext, email_body)
588 589
589 590 except Exception:
590 591 log.error('Failed to update user password')
591 592 log.error(traceback.format_exc())
592 593 if pre_db:
593 594 # we rollback only if local db stuff fails. If it goes into
594 595 # run_task, we're pass rollback state this wouldn't work then
595 596 Session().rollback()
596 597
597 598 return True
598 599
599 600 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
600 601 """
601 602 Fetches auth_user by user_id,or api_key if present.
602 603 Fills auth_user attributes with those taken from database.
603 604 Additionally set's is_authenitated if lookup fails
604 605 present in database
605 606
606 607 :param auth_user: instance of user to set attributes
607 608 :param user_id: user id to fetch by
608 609 :param api_key: api key to fetch by
609 610 :param username: username to fetch by
610 611 """
611 612 if user_id is None and api_key is None and username is None:
612 613 raise Exception('You need to pass user_id, api_key or username')
613 614
614 615 log.debug(
615 616 'doing fill data based on: user_id:%s api_key:%s username:%s',
616 617 user_id, api_key, username)
617 618 try:
618 619 dbuser = None
619 620 if user_id:
620 621 dbuser = self.get(user_id)
621 622 elif api_key:
622 623 dbuser = self.get_by_auth_token(api_key)
623 624 elif username:
624 625 dbuser = self.get_by_username(username)
625 626
626 627 if not dbuser:
627 628 log.warning(
628 629 'Unable to lookup user by id:%s api_key:%s username:%s',
629 630 user_id, api_key, username)
630 631 return False
631 632 if not dbuser.active:
632 633 log.debug('User `%s` is inactive, skipping fill data', username)
633 634 return False
634 635
635 636 log.debug('filling user:%s data', dbuser)
636 637
637 638 # TODO: johbo: Think about this and find a clean solution
638 639 user_data = dbuser.get_dict()
639 640 user_data.update(dbuser.get_api_data(include_secrets=True))
640 641
641 642 for k, v in user_data.iteritems():
642 643 # properties of auth user we dont update
643 644 if k not in ['auth_tokens', 'permissions']:
644 645 setattr(auth_user, k, v)
645 646
646 647 # few extras
647 648 setattr(auth_user, 'feed_token', dbuser.feed_token)
648 649 except Exception:
649 650 log.error(traceback.format_exc())
650 651 auth_user.is_authenticated = False
651 652 return False
652 653
653 654 return True
654 655
655 656 def has_perm(self, user, perm):
656 657 perm = self._get_perm(perm)
657 658 user = self._get_user(user)
658 659
659 660 return UserToPerm.query().filter(UserToPerm.user == user)\
660 661 .filter(UserToPerm.permission == perm).scalar() is not None
661 662
662 663 def grant_perm(self, user, perm):
663 664 """
664 665 Grant user global permissions
665 666
666 667 :param user:
667 668 :param perm:
668 669 """
669 670 user = self._get_user(user)
670 671 perm = self._get_perm(perm)
671 672 # if this permission is already granted skip it
672 673 _perm = UserToPerm.query()\
673 674 .filter(UserToPerm.user == user)\
674 675 .filter(UserToPerm.permission == perm)\
675 676 .scalar()
676 677 if _perm:
677 678 return
678 679 new = UserToPerm()
679 680 new.user = user
680 681 new.permission = perm
681 682 self.sa.add(new)
682 683 return new
683 684
684 685 def revoke_perm(self, user, perm):
685 686 """
686 687 Revoke users global permissions
687 688
688 689 :param user:
689 690 :param perm:
690 691 """
691 692 user = self._get_user(user)
692 693 perm = self._get_perm(perm)
693 694
694 695 obj = UserToPerm.query()\
695 696 .filter(UserToPerm.user == user)\
696 697 .filter(UserToPerm.permission == perm)\
697 698 .scalar()
698 699 if obj:
699 700 self.sa.delete(obj)
700 701
701 702 def add_extra_email(self, user, email):
702 703 """
703 704 Adds email address to UserEmailMap
704 705
705 706 :param user:
706 707 :param email:
707 708 """
708 709 from rhodecode.model import forms
709 710 form = forms.UserExtraEmailForm()()
710 711 data = form.to_python({'email': email})
711 712 user = self._get_user(user)
712 713
713 714 obj = UserEmailMap()
714 715 obj.user = user
715 716 obj.email = data['email']
716 717 self.sa.add(obj)
717 718 return obj
718 719
719 720 def delete_extra_email(self, user, email_id):
720 721 """
721 722 Removes email address from UserEmailMap
722 723
723 724 :param user:
724 725 :param email_id:
725 726 """
726 727 user = self._get_user(user)
727 728 obj = UserEmailMap.query().get(email_id)
728 729 if obj:
729 730 self.sa.delete(obj)
730 731
731 732 def parse_ip_range(self, ip_range):
732 733 ip_list = []
733 734 def make_unique(value):
734 735 seen = []
735 736 return [c for c in value if not (c in seen or seen.append(c))]
736 737
737 738 # firsts split by commas
738 739 for ip_range in ip_range.split(','):
739 740 if not ip_range:
740 741 continue
741 742 ip_range = ip_range.strip()
742 743 if '-' in ip_range:
743 744 start_ip, end_ip = ip_range.split('-', 1)
744 745 start_ip = ipaddress.ip_address(start_ip.strip())
745 746 end_ip = ipaddress.ip_address(end_ip.strip())
746 747 parsed_ip_range = []
747 748
748 749 for index in xrange(int(start_ip), int(end_ip) + 1):
749 750 new_ip = ipaddress.ip_address(index)
750 751 parsed_ip_range.append(str(new_ip))
751 752 ip_list.extend(parsed_ip_range)
752 753 else:
753 754 ip_list.append(ip_range)
754 755
755 756 return make_unique(ip_list)
756 757
757 758 def add_extra_ip(self, user, ip, description=None):
758 759 """
759 760 Adds ip address to UserIpMap
760 761
761 762 :param user:
762 763 :param ip:
763 764 """
764 765 from rhodecode.model import forms
765 766 form = forms.UserExtraIpForm()()
766 767 data = form.to_python({'ip': ip})
767 768 user = self._get_user(user)
768 769
769 770 obj = UserIpMap()
770 771 obj.user = user
771 772 obj.ip_addr = data['ip']
772 773 obj.description = description
773 774 self.sa.add(obj)
774 775 return obj
775 776
776 777 def delete_extra_ip(self, user, ip_id):
777 778 """
778 779 Removes ip address from UserIpMap
779 780
780 781 :param user:
781 782 :param ip_id:
782 783 """
783 784 user = self._get_user(user)
784 785 obj = UserIpMap.query().get(ip_id)
785 786 if obj:
786 787 self.sa.delete(obj)
787 788
788 789 def get_accounts_in_creation_order(self, current_user=None):
789 790 """
790 791 Get accounts in order of creation for deactivation for license limits
791 792
792 793 pick currently logged in user, and append to the list in position 0
793 794 pick all super-admins in order of creation date and add it to the list
794 795 pick all other accounts in order of creation and add it to the list.
795 796
796 797 Based on that list, the last accounts can be disabled as they are
797 798 created at the end and don't include any of the super admins as well
798 799 as the current user.
799 800
800 801 :param current_user: optionally current user running this operation
801 802 """
802 803
803 804 if not current_user:
804 805 current_user = get_current_rhodecode_user()
805 806 active_super_admins = [
806 807 x.user_id for x in User.query()
807 808 .filter(User.user_id != current_user.user_id)
808 809 .filter(User.active == true())
809 810 .filter(User.admin == true())
810 811 .order_by(User.created_on.asc())]
811 812
812 813 active_regular_users = [
813 814 x.user_id for x in User.query()
814 815 .filter(User.user_id != current_user.user_id)
815 816 .filter(User.active == true())
816 817 .filter(User.admin == false())
817 818 .order_by(User.created_on.asc())]
818 819
819 820 list_of_accounts = [current_user.user_id]
820 821 list_of_accounts += active_super_admins
821 822 list_of_accounts += active_regular_users
822 823
823 824 return list_of_accounts
824 825
825 826 def deactivate_last_users(self, expected_users):
826 827 """
827 828 Deactivate accounts that are over the license limits.
828 829 Algorithm of which accounts to disabled is based on the formula:
829 830
830 831 Get current user, then super admins in creation order, then regular
831 832 active users in creation order.
832 833
833 834 Using that list we mark all accounts from the end of it as inactive.
834 835 This way we block only latest created accounts.
835 836
836 837 :param expected_users: list of users in special order, we deactivate
837 838 the end N ammoun of users from that list
838 839 """
839 840
840 841 list_of_accounts = self.get_accounts_in_creation_order()
841 842
842 843 for acc_id in list_of_accounts[expected_users + 1:]:
843 844 user = User.get(acc_id)
844 845 log.info('Deactivating account %s for license unlock', user)
845 846 user.active = False
846 847 Session().add(user)
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
@@ -1,54 +1,51 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${_('%s user settings') % c.user.username}
6 6 %if c.rhodecode_name:
7 7 &middot; ${h.branding(c.rhodecode_name)}
8 8 %endif
9 9 </%def>
10 10
11 11 <%def name="breadcrumbs_links()">
12 12 ${h.link_to(_('Admin'),h.url('admin_home'))}
13 13 &raquo;
14 14 ${h.link_to(_('Users'),h.route_path('users'))}
15 15 &raquo;
16 16 ${c.user.username}
17 17 </%def>
18 18
19 19 <%def name="menu_bar_nav()">
20 20 ${self.menu_items(active='admin')}
21 21 </%def>
22 22
23 23 <%def name="main()">
24 24 <div class="box user_settings">
25 25 <div class="title">
26 26 ${self.breadcrumbs()}
27 27 </div>
28 28
29 29 ##main
30 30 <div class="sidebar-col-wrapper">
31 31 <div class="sidebar">
32 32 <ul class="nav nav-pills nav-stacked">
33 33 <li class="${'active' if c.active=='profile' else ''}"><a href="${h.url('edit_user', user_id=c.user.user_id)}">${_('User Profile')}</a></li>
34 34 <li class="${'active' if c.active=='auth_tokens' else ''}"><a href="${h.route_path('edit_user_auth_tokens', user_id=c.user.user_id)}">${_('Auth tokens')}</a></li>
35 35 <li class="${'active' if c.active=='advanced' else ''}"><a href="${h.url('edit_user_advanced', user_id=c.user.user_id)}">${_('Advanced')}</a></li>
36 36 <li class="${'active' if c.active=='global_perms' else ''}"><a href="${h.url('edit_user_global_perms', user_id=c.user.user_id)}">${_('Global permissions')}</a></li>
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
48 45 <div class="main-content-full-width">
49 46 <%include file="/admin/users/user_edit_${c.active}.mako"/>
50 47 </div>
51 48 </div>
52 49 </div>
53 50
54 51 </%def>
General Comments 0
You need to be logged in to leave comments. Login now