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