##// END OF EJS Templates
user-groups: moved the display of user group into a pyramid view
marcink -
r1980:f55ac84b default
parent child Browse files
Show More
@@ -0,0 +1,113 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 pytest
22
23 from rhodecode.model.db import UserGroup, User
24 from rhodecode.model.meta import Session
25
26 from rhodecode.tests import (
27 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
28 from rhodecode.tests.fixture import Fixture
29
30 fixture = Fixture()
31
32
33 def route_path(name, params=None, **kwargs):
34 import urllib
35 from rhodecode.apps._base import ADMIN_PREFIX
36
37 base_url = {
38 'user_groups': ADMIN_PREFIX + '/user_groups',
39 'user_groups_data': ADMIN_PREFIX + '/user_groups_data',
40 'user_group_members_data': ADMIN_PREFIX + '/user_groups/{user_group_id}/members',
41 }[name].format(**kwargs)
42
43 if params:
44 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
45 return base_url
46
47
48 class TestAdminUserGroupsView(TestController):
49
50 def test_show_users(self):
51 self.log_user()
52 self.app.get(route_path('user_groups'))
53
54 def test_show_user_groups_data(self, xhr_header):
55 self.log_user()
56 response = self.app.get(route_path(
57 'user_groups_data'), extra_environ=xhr_header)
58
59 all_user_groups = UserGroup.query().count()
60 assert response.json['recordsTotal'] == all_user_groups
61
62 def test_show_user_groups_data_filtered(self, xhr_header):
63 self.log_user()
64 response = self.app.get(route_path(
65 'user_groups_data', params={'search[value]': 'empty_search'}),
66 extra_environ=xhr_header)
67
68 all_user_groups = UserGroup.query().count()
69 assert response.json['recordsTotal'] == all_user_groups
70 assert response.json['recordsFiltered'] == 0
71
72 def test_usergroup_escape(self, user_util, xhr_header):
73 self.log_user()
74
75 xss_img = '<img src="/image1" onload="alert(\'Hello, World!\');">'
76 user = user_util.create_user()
77 user.name = xss_img
78 user.lastname = xss_img
79 Session().add(user)
80 Session().commit()
81
82 user_group = user_util.create_user_group()
83
84 user_group.users_group_name = xss_img
85 user_group.user_group_description = '<strong onload="alert();">DESC</strong>'
86
87 response = self.app.get(
88 route_path('user_groups_data'), extra_environ=xhr_header)
89
90 response.mustcontain(
91 '&lt;strong onload=&#34;alert();&#34;&gt;DESC&lt;/strong&gt;')
92 response.mustcontain(
93 '&lt;img src=&#34;/image1&#34; onload=&#34;'
94 'alert(&#39;Hello, World!&#39;);&#34;&gt;')
95
96 def test_edit_user_group_autocomplete_empty_members(self, xhr_header, user_util):
97 self.log_user()
98 ug = user_util.create_user_group()
99 response = self.app.get(
100 route_path('user_group_members_data', user_group_id=ug.users_group_id),
101 extra_environ=xhr_header)
102
103 assert response.json == {'members': []}
104
105 def test_edit_user_group_autocomplete_members(self, xhr_header, user_util):
106 self.log_user()
107 members = [u.user_id for u in User.get_all()]
108 ug = user_util.create_user_group(members=members)
109 response = self.app.get(
110 route_path('user_group_members_data', user_group_id=ug.users_group_id),
111 extra_environ=xhr_header)
112
113 assert len(response.json['members']) == len(members)
@@ -0,0 +1,195 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-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 import datetime
23
24 from pyramid.httpexceptions import HTTPFound
25 from pyramid.view import view_config
26
27 from rhodecode.lib.helpers import Page
28 from rhodecode.model.scm import UserGroupList
29 from rhodecode_tools.lib.ext_json import json
30
31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 from rhodecode.lib.auth import (
33 LoginRequired, HasPermissionAllDecorator, CSRFRequired, NotAnonymous,
34 HasUserGroupPermissionAnyDecorator)
35 from rhodecode.lib import helpers as h
36 from rhodecode.lib.utils import PartialRenderer
37 from rhodecode.lib.utils2 import safe_int, safe_unicode
38 from rhodecode.model.auth_token import AuthTokenModel
39 from rhodecode.model.user import UserModel
40 from rhodecode.model.user_group import UserGroupModel
41 from rhodecode.model.db import User, UserGroup, UserGroupMember, or_, count
42 from rhodecode.model.meta import Session
43
44 log = logging.getLogger(__name__)
45
46
47 class AdminUserGroupsView(BaseAppView, DataGridAppView):
48
49 def load_default_context(self):
50 c = self._get_local_tmpl_context()
51 self._register_global_c(c)
52 return c
53
54 # permission check in data loading of
55 # `user_groups_list_data` via UserGroupList
56 @NotAnonymous()
57 @view_config(
58 route_name='user_groups', request_method='GET',
59 renderer='rhodecode:templates/admin/user_groups/user_groups.mako')
60 def user_groups_list(self):
61 c = self.load_default_context()
62 return self._get_template_context(c)
63
64 # permission check inside
65 @NotAnonymous()
66 @view_config(
67 route_name='user_groups_data', request_method='GET',
68 renderer='json_ext', xhr=True)
69 def user_groups_list_data(self):
70 column_map = {
71 'active': 'users_group_active',
72 'description': 'user_group_description',
73 'members': 'members_total',
74 'owner': 'user_username',
75 'sync': 'group_data'
76 }
77 draw, start, limit = self._extract_chunk(self.request)
78 search_q, order_by, order_dir = self._extract_ordering(
79 self.request, column_map=column_map)
80
81 _render = PartialRenderer('data_table/_dt_elements.mako')
82
83 def user_group_name(user_group_id, user_group_name):
84 return _render("user_group_name", user_group_id, user_group_name)
85
86 def user_group_actions(user_group_id, user_group_name):
87 return _render("user_group_actions", user_group_id, user_group_name)
88
89 def user_profile(username):
90 return _render('user_profile', username)
91
92 user_groups_data_total_count = UserGroup.query().count()
93
94 member_count = count(UserGroupMember.user_id)
95 base_q = Session.query(
96 UserGroup.users_group_name,
97 UserGroup.user_group_description,
98 UserGroup.users_group_active,
99 UserGroup.users_group_id,
100 UserGroup.group_data,
101 User,
102 member_count.label('member_count')
103 ) \
104 .outerjoin(UserGroupMember) \
105 .join(User, User.user_id == UserGroup.user_id) \
106 .group_by(UserGroup, User)
107
108 if search_q:
109 like_expression = u'%{}%'.format(safe_unicode(search_q))
110 base_q = base_q.filter(or_(
111 UserGroup.users_group_name.ilike(like_expression),
112 ))
113
114 user_groups_data_total_filtered_count = base_q.count()
115
116 if order_by == 'members_total':
117 sort_col = member_count
118 elif order_by == 'user_username':
119 sort_col = User.username
120 else:
121 sort_col = getattr(UserGroup, order_by, None)
122
123 if isinstance(sort_col, count) or sort_col:
124 if order_dir == 'asc':
125 sort_col = sort_col.asc()
126 else:
127 sort_col = sort_col.desc()
128
129 base_q = base_q.order_by(sort_col)
130 base_q = base_q.offset(start).limit(limit)
131
132 # authenticated access to user groups
133 user_group_list = base_q.all()
134 auth_user_group_list = UserGroupList(
135 user_group_list, perm_set=['usergroup.admin'])
136
137 user_groups_data = []
138 for user_gr in auth_user_group_list:
139 user_groups_data.append({
140 "users_group_name": user_group_name(
141 user_gr.users_group_id, h.escape(user_gr.users_group_name)),
142 "name_raw": h.escape(user_gr.users_group_name),
143 "description": h.escape(user_gr.user_group_description),
144 "members": user_gr.member_count,
145 # NOTE(marcink): because of advanced query we
146 # need to load it like that
147 "sync": UserGroup._load_group_data(
148 user_gr.group_data).get('extern_type'),
149 "active": h.bool2icon(user_gr.users_group_active),
150 "owner": user_profile(user_gr.User.username),
151 "action": user_group_actions(
152 user_gr.users_group_id, user_gr.users_group_name)
153 })
154
155 data = ({
156 'draw': draw,
157 'data': user_groups_data,
158 'recordsTotal': user_groups_data_total_count,
159 'recordsFiltered': user_groups_data_total_filtered_count,
160 })
161
162 return data
163
164 @LoginRequired()
165 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
166 @view_config(
167 route_name='user_group_members_data', request_method='GET',
168 renderer='json_ext', xhr=True)
169 def user_group_members(self):
170 """
171 Return members of given user group
172 """
173 user_group_id = self.request.matchdict['user_group_id']
174 user_group = UserGroup.get_or_404(user_group_id)
175 group_members_obj = sorted((x.user for x in user_group.members),
176 key=lambda u: u.username.lower())
177
178 group_members = [
179 {
180 'id': user.user_id,
181 'first_name': user.first_name,
182 'last_name': user.last_name,
183 'username': user.username,
184 'icon_link': h.gravatar_url(user.email, 30),
185 'value_display': h.person(user.email),
186 'value': user.username,
187 'value_type': 'user',
188 'active': user.active,
189 }
190 for user in group_members_obj
191 ]
192
193 return {
194 'members': group_members
195 }
@@ -1,179 +1,192 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 from rhodecode.apps.admin.navigation import NavigationRegistry
23 23 from rhodecode.config.routing import ADMIN_PREFIX
24 24 from rhodecode.lib.utils2 import str2bool
25 25
26 26
27 27 def admin_routes(config):
28 28 """
29 29 Admin prefixed routes
30 30 """
31 31
32 32 config.add_route(
33 33 name='admin_audit_logs',
34 34 pattern='/audit_logs')
35 35
36 36 config.add_route(
37 37 name='pull_requests_global_0', # backward compat
38 38 pattern='/pull_requests/{pull_request_id:\d+}')
39 39 config.add_route(
40 40 name='pull_requests_global_1', # backward compat
41 41 pattern='/pull-requests/{pull_request_id:\d+}')
42 42 config.add_route(
43 43 name='pull_requests_global',
44 44 pattern='/pull-request/{pull_request_id:\d+}')
45 45
46 46 config.add_route(
47 47 name='admin_settings_open_source',
48 48 pattern='/settings/open_source')
49 49 config.add_route(
50 50 name='admin_settings_vcs_svn_generate_cfg',
51 51 pattern='/settings/vcs/svn_generate_cfg')
52 52
53 53 config.add_route(
54 54 name='admin_settings_system',
55 55 pattern='/settings/system')
56 56 config.add_route(
57 57 name='admin_settings_system_update',
58 58 pattern='/settings/system/updates')
59 59
60 60 config.add_route(
61 61 name='admin_settings_sessions',
62 62 pattern='/settings/sessions')
63 63 config.add_route(
64 64 name='admin_settings_sessions_cleanup',
65 65 pattern='/settings/sessions/cleanup')
66 66
67 67 config.add_route(
68 68 name='admin_settings_process_management',
69 69 pattern='/settings/process_management')
70 70 config.add_route(
71 71 name='admin_settings_process_management_signal',
72 72 pattern='/settings/process_management/signal')
73 73
74 74 # global permissions
75 75
76 76 config.add_route(
77 77 name='admin_permissions_application',
78 78 pattern='/permissions/application')
79 79 config.add_route(
80 80 name='admin_permissions_application_update',
81 81 pattern='/permissions/application/update')
82 82
83 83 config.add_route(
84 84 name='admin_permissions_global',
85 85 pattern='/permissions/global')
86 86 config.add_route(
87 87 name='admin_permissions_global_update',
88 88 pattern='/permissions/global/update')
89 89
90 90 config.add_route(
91 91 name='admin_permissions_object',
92 92 pattern='/permissions/object')
93 93 config.add_route(
94 94 name='admin_permissions_object_update',
95 95 pattern='/permissions/object/update')
96 96
97 97 config.add_route(
98 98 name='admin_permissions_ips',
99 99 pattern='/permissions/ips')
100 100
101 101 config.add_route(
102 102 name='admin_permissions_overview',
103 103 pattern='/permissions/overview')
104 104
105 105 config.add_route(
106 106 name='admin_permissions_auth_token_access',
107 107 pattern='/permissions/auth_token_access')
108 108
109 109 # users admin
110 110 config.add_route(
111 111 name='users',
112 112 pattern='/users')
113 113
114 114 config.add_route(
115 115 name='users_data',
116 116 pattern='/users_data')
117 117
118 118 # user auth tokens
119 119 config.add_route(
120 120 name='edit_user_auth_tokens',
121 121 pattern='/users/{user_id:\d+}/edit/auth_tokens')
122 122 config.add_route(
123 123 name='edit_user_auth_tokens_add',
124 124 pattern='/users/{user_id:\d+}/edit/auth_tokens/new')
125 125 config.add_route(
126 126 name='edit_user_auth_tokens_delete',
127 127 pattern='/users/{user_id:\d+}/edit/auth_tokens/delete')
128 128
129 129 # user emails
130 130 config.add_route(
131 131 name='edit_user_emails',
132 132 pattern='/users/{user_id:\d+}/edit/emails')
133 133 config.add_route(
134 134 name='edit_user_emails_add',
135 135 pattern='/users/{user_id:\d+}/edit/emails/new')
136 136 config.add_route(
137 137 name='edit_user_emails_delete',
138 138 pattern='/users/{user_id:\d+}/edit/emails/delete')
139 139
140 140 # user IPs
141 141 config.add_route(
142 142 name='edit_user_ips',
143 143 pattern='/users/{user_id:\d+}/edit/ips')
144 144 config.add_route(
145 145 name='edit_user_ips_add',
146 146 pattern='/users/{user_id:\d+}/edit/ips/new')
147 147 config.add_route(
148 148 name='edit_user_ips_delete',
149 149 pattern='/users/{user_id:\d+}/edit/ips/delete')
150 150
151 151 # user groups management
152 152 config.add_route(
153 153 name='edit_user_groups_management',
154 154 pattern='/users/{user_id:\d+}/edit/groups_management')
155 155
156 156 config.add_route(
157 157 name='edit_user_groups_management_updates',
158 158 pattern='/users/{user_id:\d+}/edit/edit_user_groups_management/updates')
159 159
160 160 # user audit logs
161 161 config.add_route(
162 162 name='edit_user_audit_logs',
163 163 pattern='/users/{user_id:\d+}/edit/audit')
164 164
165 # user groups admin
166 config.add_route(
167 name='user_groups',
168 pattern='/user_groups')
169
170 config.add_route(
171 name='user_groups_data',
172 pattern='/user_groups_data')
173
174 config.add_route(
175 name='user_group_members_data',
176 pattern='/user_groups/{user_group_id:\d+}/members')
177
165 178
166 179 def includeme(config):
167 180 settings = config.get_settings()
168 181
169 182 # Create admin navigation registry and add it to the pyramid registry.
170 183 labs_active = str2bool(settings.get('labs_settings_active', False))
171 184 navigation_registry = NavigationRegistry(labs_active=labs_active)
172 185 config.registry.registerUtility(navigation_registry)
173 186
174 187 # main admin routes
175 188 config.add_route(name='admin_home', pattern=ADMIN_PREFIX)
176 189 config.include(admin_routes, route_prefix=ADMIN_PREFIX)
177 190
178 191 # Scan module for configuration decorators.
179 192 config.scan()
@@ -1,505 +1,510 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23 import formencode
24 24
25 25 from pyramid.httpexceptions import HTTPFound
26 26 from pyramid.view import view_config
27 27 from sqlalchemy.sql.functions import coalesce
28 28
29 29 from rhodecode.apps._base import BaseAppView, DataGridAppView
30 30
31 31 from rhodecode.lib import audit_logger
32 32 from rhodecode.lib.ext_json import json
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
35 35 from rhodecode.lib import helpers as h
36 36 from rhodecode.lib.utils2 import safe_int, safe_unicode
37 37 from rhodecode.model.auth_token import AuthTokenModel
38 38 from rhodecode.model.user import UserModel
39 39 from rhodecode.model.user_group import UserGroupModel
40 40 from rhodecode.model.db import User, or_, UserIpMap, UserEmailMap, UserApiKeys
41 41 from rhodecode.model.meta import Session
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 class AdminUsersView(BaseAppView, DataGridAppView):
47 47 ALLOW_SCOPED_TOKENS = False
48 48 """
49 49 This view has alternative version inside EE, if modified please take a look
50 50 in there as well.
51 51 """
52 52
53 53 def load_default_context(self):
54 54 c = self._get_local_tmpl_context()
55 55 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
56 56 self._register_global_c(c)
57 57 return c
58 58
59 59 def _redirect_for_default_user(self, username):
60 60 _ = self.request.translate
61 61 if username == User.DEFAULT_USER:
62 62 h.flash(_("You can't edit this user"), category='warning')
63 63 # TODO(marcink): redirect to 'users' admin panel once this
64 64 # is a pyramid view
65 65 raise HTTPFound('/')
66 66
67 67 @HasPermissionAllDecorator('hg.admin')
68 68 @view_config(
69 69 route_name='users', request_method='GET',
70 70 renderer='rhodecode:templates/admin/users/users.mako')
71 71 def users_list(self):
72 72 c = self.load_default_context()
73 73 return self._get_template_context(c)
74 74
75 75 @HasPermissionAllDecorator('hg.admin')
76 76 @view_config(
77 77 # renderer defined below
78 78 route_name='users_data', request_method='GET',
79 79 renderer='json_ext', xhr=True)
80 80 def users_list_data(self):
81 column_map = {
82 'first_name': 'name',
83 'last_name': 'lastname',
84 }
81 85 draw, start, limit = self._extract_chunk(self.request)
82 search_q, order_by, order_dir = self._extract_ordering(self.request)
86 search_q, order_by, order_dir = self._extract_ordering(
87 self.request, column_map=column_map)
83 88
84 89 _render = self.request.get_partial_renderer(
85 90 'data_table/_dt_elements.mako')
86 91
87 92 def user_actions(user_id, username):
88 93 return _render("user_actions", user_id, username)
89 94
90 95 users_data_total_count = User.query()\
91 96 .filter(User.username != User.DEFAULT_USER) \
92 97 .count()
93 98
94 99 # json generate
95 100 base_q = User.query().filter(User.username != User.DEFAULT_USER)
96 101
97 102 if search_q:
98 103 like_expression = u'%{}%'.format(safe_unicode(search_q))
99 104 base_q = base_q.filter(or_(
100 105 User.username.ilike(like_expression),
101 106 User._email.ilike(like_expression),
102 107 User.name.ilike(like_expression),
103 108 User.lastname.ilike(like_expression),
104 109 ))
105 110
106 111 users_data_total_filtered_count = base_q.count()
107 112
108 113 sort_col = getattr(User, order_by, None)
109 114 if sort_col:
110 115 if order_dir == 'asc':
111 116 # handle null values properly to order by NULL last
112 117 if order_by in ['last_activity']:
113 118 sort_col = coalesce(sort_col, datetime.date.max)
114 119 sort_col = sort_col.asc()
115 120 else:
116 121 # handle null values properly to order by NULL last
117 122 if order_by in ['last_activity']:
118 123 sort_col = coalesce(sort_col, datetime.date.min)
119 124 sort_col = sort_col.desc()
120 125
121 126 base_q = base_q.order_by(sort_col)
122 127 base_q = base_q.offset(start).limit(limit)
123 128
124 129 users_list = base_q.all()
125 130
126 131 users_data = []
127 132 for user in users_list:
128 133 users_data.append({
129 134 "username": h.gravatar_with_user(self.request, user.username),
130 135 "email": user.email,
131 136 "first_name": user.first_name,
132 137 "last_name": user.last_name,
133 138 "last_login": h.format_date(user.last_login),
134 139 "last_activity": h.format_date(user.last_activity),
135 140 "active": h.bool2icon(user.active),
136 141 "active_raw": user.active,
137 142 "admin": h.bool2icon(user.admin),
138 143 "extern_type": user.extern_type,
139 144 "extern_name": user.extern_name,
140 145 "action": user_actions(user.user_id, user.username),
141 146 })
142 147
143 148 data = ({
144 149 'draw': draw,
145 150 'data': users_data,
146 151 'recordsTotal': users_data_total_count,
147 152 'recordsFiltered': users_data_total_filtered_count,
148 153 })
149 154
150 155 return data
151 156
152 157 @LoginRequired()
153 158 @HasPermissionAllDecorator('hg.admin')
154 159 @view_config(
155 160 route_name='edit_user_auth_tokens', request_method='GET',
156 161 renderer='rhodecode:templates/admin/users/user_edit.mako')
157 162 def auth_tokens(self):
158 163 _ = self.request.translate
159 164 c = self.load_default_context()
160 165
161 166 user_id = self.request.matchdict.get('user_id')
162 167 c.user = User.get_or_404(user_id)
163 168 self._redirect_for_default_user(c.user.username)
164 169
165 170 c.active = 'auth_tokens'
166 171
167 172 c.lifetime_values = [
168 173 (str(-1), _('forever')),
169 174 (str(5), _('5 minutes')),
170 175 (str(60), _('1 hour')),
171 176 (str(60 * 24), _('1 day')),
172 177 (str(60 * 24 * 30), _('1 month')),
173 178 ]
174 179 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
175 180 c.role_values = [
176 181 (x, AuthTokenModel.cls._get_role_name(x))
177 182 for x in AuthTokenModel.cls.ROLES]
178 183 c.role_options = [(c.role_values, _("Role"))]
179 184 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
180 185 c.user.user_id, show_expired=True)
181 186 return self._get_template_context(c)
182 187
183 188 def maybe_attach_token_scope(self, token):
184 189 # implemented in EE edition
185 190 pass
186 191
187 192 @LoginRequired()
188 193 @HasPermissionAllDecorator('hg.admin')
189 194 @CSRFRequired()
190 195 @view_config(
191 196 route_name='edit_user_auth_tokens_add', request_method='POST')
192 197 def auth_tokens_add(self):
193 198 _ = self.request.translate
194 199 c = self.load_default_context()
195 200
196 201 user_id = self.request.matchdict.get('user_id')
197 202 c.user = User.get_or_404(user_id)
198 203
199 204 self._redirect_for_default_user(c.user.username)
200 205
201 206 user_data = c.user.get_api_data()
202 207 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
203 208 description = self.request.POST.get('description')
204 209 role = self.request.POST.get('role')
205 210
206 211 token = AuthTokenModel().create(
207 212 c.user.user_id, description, lifetime, role)
208 213 token_data = token.get_api_data()
209 214
210 215 self.maybe_attach_token_scope(token)
211 216 audit_logger.store_web(
212 217 'user.edit.token.add', action_data={
213 218 'data': {'token': token_data, 'user': user_data}},
214 219 user=self._rhodecode_user, )
215 220 Session().commit()
216 221
217 222 h.flash(_("Auth token successfully created"), category='success')
218 223 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
219 224
220 225 @LoginRequired()
221 226 @HasPermissionAllDecorator('hg.admin')
222 227 @CSRFRequired()
223 228 @view_config(
224 229 route_name='edit_user_auth_tokens_delete', request_method='POST')
225 230 def auth_tokens_delete(self):
226 231 _ = self.request.translate
227 232 c = self.load_default_context()
228 233
229 234 user_id = self.request.matchdict.get('user_id')
230 235 c.user = User.get_or_404(user_id)
231 236 self._redirect_for_default_user(c.user.username)
232 237 user_data = c.user.get_api_data()
233 238
234 239 del_auth_token = self.request.POST.get('del_auth_token')
235 240
236 241 if del_auth_token:
237 242 token = UserApiKeys.get_or_404(del_auth_token)
238 243 token_data = token.get_api_data()
239 244
240 245 AuthTokenModel().delete(del_auth_token, c.user.user_id)
241 246 audit_logger.store_web(
242 247 'user.edit.token.delete', action_data={
243 248 'data': {'token': token_data, 'user': user_data}},
244 249 user=self._rhodecode_user,)
245 250 Session().commit()
246 251 h.flash(_("Auth token successfully deleted"), category='success')
247 252
248 253 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
249 254
250 255 @LoginRequired()
251 256 @HasPermissionAllDecorator('hg.admin')
252 257 @view_config(
253 258 route_name='edit_user_emails', request_method='GET',
254 259 renderer='rhodecode:templates/admin/users/user_edit.mako')
255 260 def emails(self):
256 261 _ = self.request.translate
257 262 c = self.load_default_context()
258 263
259 264 user_id = self.request.matchdict.get('user_id')
260 265 c.user = User.get_or_404(user_id)
261 266 self._redirect_for_default_user(c.user.username)
262 267
263 268 c.active = 'emails'
264 269 c.user_email_map = UserEmailMap.query() \
265 270 .filter(UserEmailMap.user == c.user).all()
266 271
267 272 return self._get_template_context(c)
268 273
269 274 @LoginRequired()
270 275 @HasPermissionAllDecorator('hg.admin')
271 276 @CSRFRequired()
272 277 @view_config(
273 278 route_name='edit_user_emails_add', request_method='POST')
274 279 def emails_add(self):
275 280 _ = self.request.translate
276 281 c = self.load_default_context()
277 282
278 283 user_id = self.request.matchdict.get('user_id')
279 284 c.user = User.get_or_404(user_id)
280 285 self._redirect_for_default_user(c.user.username)
281 286
282 287 email = self.request.POST.get('new_email')
283 288 user_data = c.user.get_api_data()
284 289 try:
285 290 UserModel().add_extra_email(c.user.user_id, email)
286 291 audit_logger.store_web(
287 292 'user.edit.email.add', action_data={'email': email, 'user': user_data},
288 293 user=self._rhodecode_user)
289 294 Session().commit()
290 295 h.flash(_("Added new email address `%s` for user account") % email,
291 296 category='success')
292 297 except formencode.Invalid as error:
293 298 h.flash(h.escape(error.error_dict['email']), category='error')
294 299 except Exception:
295 300 log.exception("Exception during email saving")
296 301 h.flash(_('An error occurred during email saving'),
297 302 category='error')
298 303 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
299 304
300 305 @LoginRequired()
301 306 @HasPermissionAllDecorator('hg.admin')
302 307 @CSRFRequired()
303 308 @view_config(
304 309 route_name='edit_user_emails_delete', request_method='POST')
305 310 def emails_delete(self):
306 311 _ = self.request.translate
307 312 c = self.load_default_context()
308 313
309 314 user_id = self.request.matchdict.get('user_id')
310 315 c.user = User.get_or_404(user_id)
311 316 self._redirect_for_default_user(c.user.username)
312 317
313 318 email_id = self.request.POST.get('del_email_id')
314 319 user_model = UserModel()
315 320
316 321 email = UserEmailMap.query().get(email_id).email
317 322 user_data = c.user.get_api_data()
318 323 user_model.delete_extra_email(c.user.user_id, email_id)
319 324 audit_logger.store_web(
320 325 'user.edit.email.delete', action_data={'email': email, 'user': user_data},
321 326 user=self._rhodecode_user)
322 327 Session().commit()
323 328 h.flash(_("Removed email address from user account"),
324 329 category='success')
325 330 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
326 331
327 332 @LoginRequired()
328 333 @HasPermissionAllDecorator('hg.admin')
329 334 @view_config(
330 335 route_name='edit_user_ips', request_method='GET',
331 336 renderer='rhodecode:templates/admin/users/user_edit.mako')
332 337 def ips(self):
333 338 _ = self.request.translate
334 339 c = self.load_default_context()
335 340
336 341 user_id = self.request.matchdict.get('user_id')
337 342 c.user = User.get_or_404(user_id)
338 343 self._redirect_for_default_user(c.user.username)
339 344
340 345 c.active = 'ips'
341 346 c.user_ip_map = UserIpMap.query() \
342 347 .filter(UserIpMap.user == c.user).all()
343 348
344 349 c.inherit_default_ips = c.user.inherit_default_permissions
345 350 c.default_user_ip_map = UserIpMap.query() \
346 351 .filter(UserIpMap.user == User.get_default_user()).all()
347 352
348 353 return self._get_template_context(c)
349 354
350 355 @LoginRequired()
351 356 @HasPermissionAllDecorator('hg.admin')
352 357 @CSRFRequired()
353 358 @view_config(
354 359 route_name='edit_user_ips_add', request_method='POST')
355 360 def ips_add(self):
356 361 _ = self.request.translate
357 362 c = self.load_default_context()
358 363
359 364 user_id = self.request.matchdict.get('user_id')
360 365 c.user = User.get_or_404(user_id)
361 366 # NOTE(marcink): this view is allowed for default users, as we can
362 367 # edit their IP white list
363 368
364 369 user_model = UserModel()
365 370 desc = self.request.POST.get('description')
366 371 try:
367 372 ip_list = user_model.parse_ip_range(
368 373 self.request.POST.get('new_ip'))
369 374 except Exception as e:
370 375 ip_list = []
371 376 log.exception("Exception during ip saving")
372 377 h.flash(_('An error occurred during ip saving:%s' % (e,)),
373 378 category='error')
374 379 added = []
375 380 user_data = c.user.get_api_data()
376 381 for ip in ip_list:
377 382 try:
378 383 user_model.add_extra_ip(c.user.user_id, ip, desc)
379 384 audit_logger.store_web(
380 385 'user.edit.ip.add', action_data={'ip': ip, 'user': user_data},
381 386 user=self._rhodecode_user)
382 387 Session().commit()
383 388 added.append(ip)
384 389 except formencode.Invalid as error:
385 390 msg = error.error_dict['ip']
386 391 h.flash(msg, category='error')
387 392 except Exception:
388 393 log.exception("Exception during ip saving")
389 394 h.flash(_('An error occurred during ip saving'),
390 395 category='error')
391 396 if added:
392 397 h.flash(
393 398 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
394 399 category='success')
395 400 if 'default_user' in self.request.POST:
396 401 # case for editing global IP list we do it for 'DEFAULT' user
397 402 raise HTTPFound(h.route_path('admin_permissions_ips'))
398 403 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
399 404
400 405 @LoginRequired()
401 406 @HasPermissionAllDecorator('hg.admin')
402 407 @CSRFRequired()
403 408 @view_config(
404 409 route_name='edit_user_ips_delete', request_method='POST')
405 410 def ips_delete(self):
406 411 _ = self.request.translate
407 412 c = self.load_default_context()
408 413
409 414 user_id = self.request.matchdict.get('user_id')
410 415 c.user = User.get_or_404(user_id)
411 416 # NOTE(marcink): this view is allowed for default users, as we can
412 417 # edit their IP white list
413 418
414 419 ip_id = self.request.POST.get('del_ip_id')
415 420 user_model = UserModel()
416 421 user_data = c.user.get_api_data()
417 422 ip = UserIpMap.query().get(ip_id).ip_addr
418 423 user_model.delete_extra_ip(c.user.user_id, ip_id)
419 424 audit_logger.store_web(
420 425 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
421 426 user=self._rhodecode_user)
422 427 Session().commit()
423 428 h.flash(_("Removed ip address from user whitelist"), category='success')
424 429
425 430 if 'default_user' in self.request.POST:
426 431 # case for editing global IP list we do it for 'DEFAULT' user
427 432 raise HTTPFound(h.route_path('admin_permissions_ips'))
428 433 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
429 434
430 435 @LoginRequired()
431 436 @HasPermissionAllDecorator('hg.admin')
432 437 @view_config(
433 438 route_name='edit_user_groups_management', request_method='GET',
434 439 renderer='rhodecode:templates/admin/users/user_edit.mako')
435 440 def groups_management(self):
436 441 c = self.load_default_context()
437 442
438 443 user_id = self.request.matchdict.get('user_id')
439 444 c.user = User.get_or_404(user_id)
440 445 c.data = c.user.group_member
441 446 self._redirect_for_default_user(c.user.username)
442 447 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
443 448 for group in c.user.group_member]
444 449 c.groups = json.dumps(groups)
445 450 c.active = 'groups'
446 451
447 452 return self._get_template_context(c)
448 453
449 454 @LoginRequired()
450 455 @HasPermissionAllDecorator('hg.admin')
451 456 @CSRFRequired()
452 457 @view_config(
453 458 route_name='edit_user_groups_management_updates', request_method='POST')
454 459 def groups_management_updates(self):
455 460 _ = self.request.translate
456 461 c = self.load_default_context()
457 462
458 463 user_id = self.request.matchdict.get('user_id')
459 464 c.user = User.get_or_404(user_id)
460 465 self._redirect_for_default_user(c.user.username)
461 466
462 467 users_groups = set(self.request.POST.getall('users_group_id'))
463 468 users_groups_model = []
464 469
465 470 for ugid in users_groups:
466 471 users_groups_model.append(UserGroupModel().get_group(safe_int(ugid)))
467 472 user_group_model = UserGroupModel()
468 473 user_group_model.change_groups(c.user, users_groups_model)
469 474
470 475 Session().commit()
471 476 c.active = 'user_groups_management'
472 477 h.flash(_("Groups successfully changed"), category='success')
473 478
474 479 return HTTPFound(h.route_path(
475 480 'edit_user_groups_management', user_id=user_id))
476 481
477 482 @LoginRequired()
478 483 @HasPermissionAllDecorator('hg.admin')
479 484 @view_config(
480 485 route_name='edit_user_audit_logs', request_method='GET',
481 486 renderer='rhodecode:templates/admin/users/user_edit.mako')
482 487 def user_audit_logs(self):
483 488 _ = self.request.translate
484 489 c = self.load_default_context()
485 490
486 491 user_id = self.request.matchdict.get('user_id')
487 492 c.user = User.get_or_404(user_id)
488 493 self._redirect_for_default_user(c.user.username)
489 494 c.active = 'audit'
490 495
491 496 p = safe_int(self.request.GET.get('page', 1), 1)
492 497
493 498 filter_term = self.request.GET.get('filter')
494 499 user_log = UserModel().get_user_log(c.user, filter_term)
495 500
496 501 def url_generator(**kw):
497 502 if filter_term:
498 503 kw['filter'] = filter_term
499 504 return self.request.current_route_path(_query=kw)
500 505
501 506 c.audit_logs = h.Page(
502 507 user_log, page=p, items_per_page=10, url=url_generator)
503 508 c.filter_term = filter_term
504 509 return self._get_template_context(c)
505 510
@@ -1,521 +1,515 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Routes configuration
23 23
24 24 The more specific and detailed routes should be defined first so they
25 25 may take precedent over the more generic routes. For more information
26 26 refer to the routes manual at http://routes.groovie.org/docs/
27 27
28 28 IMPORTANT: if you change any routing here, make sure to take a look at lib/base.py
29 29 and _route_name variable which uses some of stored naming here to do redirects.
30 30 """
31 31 import os
32 32 import re
33 33 from routes import Mapper
34 34
35 35 # prefix for non repository related links needs to be prefixed with `/`
36 36 ADMIN_PREFIX = '/_admin'
37 37 STATIC_FILE_PREFIX = '/_static'
38 38
39 39 # Default requirements for URL parts
40 40 URL_NAME_REQUIREMENTS = {
41 41 # group name can have a slash in them, but they must not end with a slash
42 42 'group_name': r'.*?[^/]',
43 43 'repo_group_name': r'.*?[^/]',
44 44 # repo names can have a slash in them, but they must not end with a slash
45 45 'repo_name': r'.*?[^/]',
46 46 # file path eats up everything at the end
47 47 'f_path': r'.*',
48 48 # reference types
49 49 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
50 50 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
51 51 }
52 52
53 53
54 54 class JSRoutesMapper(Mapper):
55 55 """
56 56 Wrapper for routes.Mapper to make pyroutes compatible url definitions
57 57 """
58 58 _named_route_regex = re.compile(r'^[a-z-_0-9A-Z]+$')
59 59 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
60 60 def __init__(self, *args, **kw):
61 61 super(JSRoutesMapper, self).__init__(*args, **kw)
62 62 self._jsroutes = []
63 63
64 64 def connect(self, *args, **kw):
65 65 """
66 66 Wrapper for connect to take an extra argument jsroute=True
67 67
68 68 :param jsroute: boolean, if True will add the route to the pyroutes list
69 69 """
70 70 if kw.pop('jsroute', False):
71 71 if not self._named_route_regex.match(args[0]):
72 72 raise Exception('only named routes can be added to pyroutes')
73 73 self._jsroutes.append(args[0])
74 74
75 75 super(JSRoutesMapper, self).connect(*args, **kw)
76 76
77 77 def _extract_route_information(self, route):
78 78 """
79 79 Convert a route into tuple(name, path, args), eg:
80 80 ('show_user', '/profile/%(username)s', ['username'])
81 81 """
82 82 routepath = route.routepath
83 83 def replace(matchobj):
84 84 if matchobj.group(1):
85 85 return "%%(%s)s" % matchobj.group(1).split(':')[0]
86 86 else:
87 87 return "%%(%s)s" % matchobj.group(2)
88 88
89 89 routepath = self._argument_prog.sub(replace, routepath)
90 90 return (
91 91 route.name,
92 92 routepath,
93 93 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
94 94 for arg in self._argument_prog.findall(route.routepath)]
95 95 )
96 96
97 97 def jsroutes(self):
98 98 """
99 99 Return a list of pyroutes.js compatible routes
100 100 """
101 101 for route_name in self._jsroutes:
102 102 yield self._extract_route_information(self._routenames[route_name])
103 103
104 104
105 105 def make_map(config):
106 106 """Create, configure and return the routes Mapper"""
107 107 rmap = JSRoutesMapper(
108 108 directory=config['pylons.paths']['controllers'],
109 109 always_scan=config['debug'])
110 110 rmap.minimization = False
111 111 rmap.explicit = False
112 112
113 113 from rhodecode.lib.utils2 import str2bool
114 114 from rhodecode.model import repo, repo_group
115 115
116 116 def check_repo(environ, match_dict):
117 117 """
118 118 check for valid repository for proper 404 handling
119 119
120 120 :param environ:
121 121 :param match_dict:
122 122 """
123 123 repo_name = match_dict.get('repo_name')
124 124
125 125 if match_dict.get('f_path'):
126 126 # fix for multiple initial slashes that causes errors
127 127 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
128 128 repo_model = repo.RepoModel()
129 129 by_name_match = repo_model.get_by_repo_name(repo_name)
130 130 # if we match quickly from database, short circuit the operation,
131 131 # and validate repo based on the type.
132 132 if by_name_match:
133 133 return True
134 134
135 135 by_id_match = repo_model.get_repo_by_id(repo_name)
136 136 if by_id_match:
137 137 repo_name = by_id_match.repo_name
138 138 match_dict['repo_name'] = repo_name
139 139 return True
140 140
141 141 return False
142 142
143 143 def check_group(environ, match_dict):
144 144 """
145 145 check for valid repository group path for proper 404 handling
146 146
147 147 :param environ:
148 148 :param match_dict:
149 149 """
150 150 repo_group_name = match_dict.get('group_name')
151 151 repo_group_model = repo_group.RepoGroupModel()
152 152 by_name_match = repo_group_model.get_by_group_name(repo_group_name)
153 153 if by_name_match:
154 154 return True
155 155
156 156 return False
157 157
158 158 def check_user_group(environ, match_dict):
159 159 """
160 160 check for valid user group for proper 404 handling
161 161
162 162 :param environ:
163 163 :param match_dict:
164 164 """
165 165 return True
166 166
167 167 def check_int(environ, match_dict):
168 168 return match_dict.get('id').isdigit()
169 169
170 170
171 171 #==========================================================================
172 172 # CUSTOM ROUTES HERE
173 173 #==========================================================================
174 174
175 175 # ping and pylons error test
176 176 rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping')
177 177 rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test')
178 178
179 179 # ADMIN REPOSITORY ROUTES
180 180 with rmap.submapper(path_prefix=ADMIN_PREFIX,
181 181 controller='admin/repos') as m:
182 182 m.connect('repos', '/repos',
183 183 action='create', conditions={'method': ['POST']})
184 184 m.connect('repos', '/repos',
185 185 action='index', conditions={'method': ['GET']})
186 186 m.connect('new_repo', '/create_repository', jsroute=True,
187 187 action='create_repository', conditions={'method': ['GET']})
188 188 m.connect('delete_repo', '/repos/{repo_name}',
189 189 action='delete', conditions={'method': ['DELETE']},
190 190 requirements=URL_NAME_REQUIREMENTS)
191 191 m.connect('repo', '/repos/{repo_name}',
192 192 action='show', conditions={'method': ['GET'],
193 193 'function': check_repo},
194 194 requirements=URL_NAME_REQUIREMENTS)
195 195
196 196 # ADMIN REPOSITORY GROUPS ROUTES
197 197 with rmap.submapper(path_prefix=ADMIN_PREFIX,
198 198 controller='admin/repo_groups') as m:
199 199 m.connect('repo_groups', '/repo_groups',
200 200 action='create', conditions={'method': ['POST']})
201 201 m.connect('repo_groups', '/repo_groups',
202 202 action='index', conditions={'method': ['GET']})
203 203 m.connect('new_repo_group', '/repo_groups/new',
204 204 action='new', conditions={'method': ['GET']})
205 205 m.connect('update_repo_group', '/repo_groups/{group_name}',
206 206 action='update', conditions={'method': ['PUT'],
207 207 'function': check_group},
208 208 requirements=URL_NAME_REQUIREMENTS)
209 209
210 210 # EXTRAS REPO GROUP ROUTES
211 211 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
212 212 action='edit',
213 213 conditions={'method': ['GET'], 'function': check_group},
214 214 requirements=URL_NAME_REQUIREMENTS)
215 215 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
216 216 action='edit',
217 217 conditions={'method': ['PUT'], 'function': check_group},
218 218 requirements=URL_NAME_REQUIREMENTS)
219 219
220 220 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
221 221 action='edit_repo_group_advanced',
222 222 conditions={'method': ['GET'], 'function': check_group},
223 223 requirements=URL_NAME_REQUIREMENTS)
224 224 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
225 225 action='edit_repo_group_advanced',
226 226 conditions={'method': ['PUT'], 'function': check_group},
227 227 requirements=URL_NAME_REQUIREMENTS)
228 228
229 229 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
230 230 action='edit_repo_group_perms',
231 231 conditions={'method': ['GET'], 'function': check_group},
232 232 requirements=URL_NAME_REQUIREMENTS)
233 233 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
234 234 action='update_perms',
235 235 conditions={'method': ['PUT'], 'function': check_group},
236 236 requirements=URL_NAME_REQUIREMENTS)
237 237
238 238 m.connect('delete_repo_group', '/repo_groups/{group_name}',
239 239 action='delete', conditions={'method': ['DELETE'],
240 240 'function': check_group},
241 241 requirements=URL_NAME_REQUIREMENTS)
242 242
243 243 # ADMIN USER ROUTES
244 244 with rmap.submapper(path_prefix=ADMIN_PREFIX,
245 245 controller='admin/users') as m:
246 246 m.connect('users', '/users',
247 247 action='create', conditions={'method': ['POST']})
248 248 m.connect('new_user', '/users/new',
249 249 action='new', conditions={'method': ['GET']})
250 250 m.connect('update_user', '/users/{user_id}',
251 251 action='update', conditions={'method': ['PUT']})
252 252 m.connect('delete_user', '/users/{user_id}',
253 253 action='delete', conditions={'method': ['DELETE']})
254 254 m.connect('edit_user', '/users/{user_id}/edit',
255 255 action='edit', conditions={'method': ['GET']}, jsroute=True)
256 256 m.connect('user', '/users/{user_id}',
257 257 action='show', conditions={'method': ['GET']})
258 258 m.connect('force_password_reset_user', '/users/{user_id}/password_reset',
259 259 action='reset_password', conditions={'method': ['POST']})
260 260 m.connect('create_personal_repo_group', '/users/{user_id}/create_repo_group',
261 261 action='create_personal_repo_group', conditions={'method': ['POST']})
262 262
263 263 # EXTRAS USER ROUTES
264 264 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
265 265 action='edit_advanced', conditions={'method': ['GET']})
266 266 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
267 267 action='update_advanced', conditions={'method': ['PUT']})
268 268
269 269 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
270 270 action='edit_global_perms', conditions={'method': ['GET']})
271 271 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
272 272 action='update_global_perms', conditions={'method': ['PUT']})
273 273
274 274 m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary',
275 275 action='edit_perms_summary', conditions={'method': ['GET']})
276 276
277 277 # ADMIN USER GROUPS REST ROUTES
278 278 with rmap.submapper(path_prefix=ADMIN_PREFIX,
279 279 controller='admin/user_groups') as m:
280 280 m.connect('users_groups', '/user_groups',
281 281 action='create', conditions={'method': ['POST']})
282 m.connect('users_groups', '/user_groups',
283 action='index', conditions={'method': ['GET']})
284 282 m.connect('new_users_group', '/user_groups/new',
285 283 action='new', conditions={'method': ['GET']})
286 284 m.connect('update_users_group', '/user_groups/{user_group_id}',
287 285 action='update', conditions={'method': ['PUT']})
288 286 m.connect('delete_users_group', '/user_groups/{user_group_id}',
289 287 action='delete', conditions={'method': ['DELETE']})
290 288 m.connect('edit_users_group', '/user_groups/{user_group_id}/edit',
291 289 action='edit', conditions={'method': ['GET']},
292 290 function=check_user_group)
293 291
294 292 # EXTRAS USER GROUP ROUTES
295 293 m.connect('edit_user_group_global_perms',
296 294 '/user_groups/{user_group_id}/edit/global_permissions',
297 295 action='edit_global_perms', conditions={'method': ['GET']})
298 296 m.connect('edit_user_group_global_perms',
299 297 '/user_groups/{user_group_id}/edit/global_permissions',
300 298 action='update_global_perms', conditions={'method': ['PUT']})
301 299 m.connect('edit_user_group_perms_summary',
302 300 '/user_groups/{user_group_id}/edit/permissions_summary',
303 301 action='edit_perms_summary', conditions={'method': ['GET']})
304 302
305 303 m.connect('edit_user_group_perms',
306 304 '/user_groups/{user_group_id}/edit/permissions',
307 305 action='edit_perms', conditions={'method': ['GET']})
308 306 m.connect('edit_user_group_perms',
309 307 '/user_groups/{user_group_id}/edit/permissions',
310 308 action='update_perms', conditions={'method': ['PUT']})
311 309
312 310 m.connect('edit_user_group_advanced',
313 311 '/user_groups/{user_group_id}/edit/advanced',
314 312 action='edit_advanced', conditions={'method': ['GET']})
315 313
316 314 m.connect('edit_user_group_advanced_sync',
317 315 '/user_groups/{user_group_id}/edit/advanced/sync',
318 316 action='edit_advanced_set_synchronization', conditions={'method': ['POST']})
319 317
320 m.connect('edit_user_group_members',
321 '/user_groups/{user_group_id}/edit/members', jsroute=True,
322 action='user_group_members', conditions={'method': ['GET']})
323
324 318 # ADMIN DEFAULTS REST ROUTES
325 319 with rmap.submapper(path_prefix=ADMIN_PREFIX,
326 320 controller='admin/defaults') as m:
327 321 m.connect('admin_defaults_repositories', '/defaults/repositories',
328 322 action='update_repository_defaults', conditions={'method': ['POST']})
329 323 m.connect('admin_defaults_repositories', '/defaults/repositories',
330 324 action='index', conditions={'method': ['GET']})
331 325
332 326 # ADMIN SETTINGS ROUTES
333 327 with rmap.submapper(path_prefix=ADMIN_PREFIX,
334 328 controller='admin/settings') as m:
335 329
336 330 # default
337 331 m.connect('admin_settings', '/settings',
338 332 action='settings_global_update',
339 333 conditions={'method': ['POST']})
340 334 m.connect('admin_settings', '/settings',
341 335 action='settings_global', conditions={'method': ['GET']})
342 336
343 337 m.connect('admin_settings_vcs', '/settings/vcs',
344 338 action='settings_vcs_update',
345 339 conditions={'method': ['POST']})
346 340 m.connect('admin_settings_vcs', '/settings/vcs',
347 341 action='settings_vcs',
348 342 conditions={'method': ['GET']})
349 343 m.connect('admin_settings_vcs', '/settings/vcs',
350 344 action='delete_svn_pattern',
351 345 conditions={'method': ['DELETE']})
352 346
353 347 m.connect('admin_settings_mapping', '/settings/mapping',
354 348 action='settings_mapping_update',
355 349 conditions={'method': ['POST']})
356 350 m.connect('admin_settings_mapping', '/settings/mapping',
357 351 action='settings_mapping', conditions={'method': ['GET']})
358 352
359 353 m.connect('admin_settings_global', '/settings/global',
360 354 action='settings_global_update',
361 355 conditions={'method': ['POST']})
362 356 m.connect('admin_settings_global', '/settings/global',
363 357 action='settings_global', conditions={'method': ['GET']})
364 358
365 359 m.connect('admin_settings_visual', '/settings/visual',
366 360 action='settings_visual_update',
367 361 conditions={'method': ['POST']})
368 362 m.connect('admin_settings_visual', '/settings/visual',
369 363 action='settings_visual', conditions={'method': ['GET']})
370 364
371 365 m.connect('admin_settings_issuetracker',
372 366 '/settings/issue-tracker', action='settings_issuetracker',
373 367 conditions={'method': ['GET']})
374 368 m.connect('admin_settings_issuetracker_save',
375 369 '/settings/issue-tracker/save',
376 370 action='settings_issuetracker_save',
377 371 conditions={'method': ['POST']})
378 372 m.connect('admin_issuetracker_test', '/settings/issue-tracker/test',
379 373 action='settings_issuetracker_test',
380 374 conditions={'method': ['POST']})
381 375 m.connect('admin_issuetracker_delete',
382 376 '/settings/issue-tracker/delete',
383 377 action='settings_issuetracker_delete',
384 378 conditions={'method': ['DELETE']})
385 379
386 380 m.connect('admin_settings_email', '/settings/email',
387 381 action='settings_email_update',
388 382 conditions={'method': ['POST']})
389 383 m.connect('admin_settings_email', '/settings/email',
390 384 action='settings_email', conditions={'method': ['GET']})
391 385
392 386 m.connect('admin_settings_hooks', '/settings/hooks',
393 387 action='settings_hooks_update',
394 388 conditions={'method': ['POST', 'DELETE']})
395 389 m.connect('admin_settings_hooks', '/settings/hooks',
396 390 action='settings_hooks', conditions={'method': ['GET']})
397 391
398 392 m.connect('admin_settings_search', '/settings/search',
399 393 action='settings_search', conditions={'method': ['GET']})
400 394
401 395 m.connect('admin_settings_supervisor', '/settings/supervisor',
402 396 action='settings_supervisor', conditions={'method': ['GET']})
403 397 m.connect('admin_settings_supervisor_log', '/settings/supervisor/{procid}/log',
404 398 action='settings_supervisor_log', conditions={'method': ['GET']})
405 399
406 400 m.connect('admin_settings_labs', '/settings/labs',
407 401 action='settings_labs_update',
408 402 conditions={'method': ['POST']})
409 403 m.connect('admin_settings_labs', '/settings/labs',
410 404 action='settings_labs', conditions={'method': ['GET']})
411 405
412 406 # ADMIN MY ACCOUNT
413 407 with rmap.submapper(path_prefix=ADMIN_PREFIX,
414 408 controller='admin/my_account') as m:
415 409
416 410 # NOTE(marcink): this needs to be kept for password force flag to be
417 411 # handled in pylons controllers, remove after full migration to pyramid
418 412 m.connect('my_account_password', '/my_account/password',
419 413 action='my_account_password', conditions={'method': ['GET']})
420 414
421 415 #==========================================================================
422 416 # REPOSITORY ROUTES
423 417 #==========================================================================
424 418
425 419 rmap.connect('repo_creating_home', '/{repo_name}/repo_creating',
426 420 controller='admin/repos', action='repo_creating',
427 421 requirements=URL_NAME_REQUIREMENTS)
428 422 rmap.connect('repo_check_home', '/{repo_name}/crepo_check',
429 423 controller='admin/repos', action='repo_check',
430 424 requirements=URL_NAME_REQUIREMENTS)
431 425
432 426 # repo edit options
433 427 rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields',
434 428 controller='admin/repos', action='edit_fields',
435 429 conditions={'method': ['GET'], 'function': check_repo},
436 430 requirements=URL_NAME_REQUIREMENTS)
437 431 rmap.connect('create_repo_fields', '/{repo_name}/settings/fields/new',
438 432 controller='admin/repos', action='create_repo_field',
439 433 conditions={'method': ['PUT'], 'function': check_repo},
440 434 requirements=URL_NAME_REQUIREMENTS)
441 435 rmap.connect('delete_repo_fields', '/{repo_name}/settings/fields/{field_id}',
442 436 controller='admin/repos', action='delete_repo_field',
443 437 conditions={'method': ['DELETE'], 'function': check_repo},
444 438 requirements=URL_NAME_REQUIREMENTS)
445 439
446 440 rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle',
447 441 controller='admin/repos', action='toggle_locking',
448 442 conditions={'method': ['GET'], 'function': check_repo},
449 443 requirements=URL_NAME_REQUIREMENTS)
450 444
451 445 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
452 446 controller='admin/repos', action='edit_remote_form',
453 447 conditions={'method': ['GET'], 'function': check_repo},
454 448 requirements=URL_NAME_REQUIREMENTS)
455 449 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
456 450 controller='admin/repos', action='edit_remote',
457 451 conditions={'method': ['PUT'], 'function': check_repo},
458 452 requirements=URL_NAME_REQUIREMENTS)
459 453
460 454 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
461 455 controller='admin/repos', action='edit_statistics_form',
462 456 conditions={'method': ['GET'], 'function': check_repo},
463 457 requirements=URL_NAME_REQUIREMENTS)
464 458 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
465 459 controller='admin/repos', action='edit_statistics',
466 460 conditions={'method': ['PUT'], 'function': check_repo},
467 461 requirements=URL_NAME_REQUIREMENTS)
468 462 rmap.connect('repo_settings_issuetracker',
469 463 '/{repo_name}/settings/issue-tracker',
470 464 controller='admin/repos', action='repo_issuetracker',
471 465 conditions={'method': ['GET'], 'function': check_repo},
472 466 requirements=URL_NAME_REQUIREMENTS)
473 467 rmap.connect('repo_issuetracker_test',
474 468 '/{repo_name}/settings/issue-tracker/test',
475 469 controller='admin/repos', action='repo_issuetracker_test',
476 470 conditions={'method': ['POST'], 'function': check_repo},
477 471 requirements=URL_NAME_REQUIREMENTS)
478 472 rmap.connect('repo_issuetracker_delete',
479 473 '/{repo_name}/settings/issue-tracker/delete',
480 474 controller='admin/repos', action='repo_issuetracker_delete',
481 475 conditions={'method': ['DELETE'], 'function': check_repo},
482 476 requirements=URL_NAME_REQUIREMENTS)
483 477 rmap.connect('repo_issuetracker_save',
484 478 '/{repo_name}/settings/issue-tracker/save',
485 479 controller='admin/repos', action='repo_issuetracker_save',
486 480 conditions={'method': ['POST'], 'function': check_repo},
487 481 requirements=URL_NAME_REQUIREMENTS)
488 482 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
489 483 controller='admin/repos', action='repo_settings_vcs_update',
490 484 conditions={'method': ['POST'], 'function': check_repo},
491 485 requirements=URL_NAME_REQUIREMENTS)
492 486 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
493 487 controller='admin/repos', action='repo_settings_vcs',
494 488 conditions={'method': ['GET'], 'function': check_repo},
495 489 requirements=URL_NAME_REQUIREMENTS)
496 490 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
497 491 controller='admin/repos', action='repo_delete_svn_pattern',
498 492 conditions={'method': ['DELETE'], 'function': check_repo},
499 493 requirements=URL_NAME_REQUIREMENTS)
500 494 rmap.connect('repo_pullrequest_settings', '/{repo_name}/settings/pullrequest',
501 495 controller='admin/repos', action='repo_settings_pullrequest',
502 496 conditions={'method': ['GET', 'POST'], 'function': check_repo},
503 497 requirements=URL_NAME_REQUIREMENTS)
504 498
505 499
506 500 rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
507 501 controller='forks', action='fork_create',
508 502 conditions={'function': check_repo, 'method': ['POST']},
509 503 requirements=URL_NAME_REQUIREMENTS)
510 504
511 505 rmap.connect('repo_fork_home', '/{repo_name}/fork',
512 506 controller='forks', action='fork',
513 507 conditions={'function': check_repo},
514 508 requirements=URL_NAME_REQUIREMENTS)
515 509
516 510 rmap.connect('repo_forks_home', '/{repo_name}/forks',
517 511 controller='forks', action='forks',
518 512 conditions={'function': check_repo},
519 513 requirements=URL_NAME_REQUIREMENTS)
520 514
521 515 return rmap
@@ -1,514 +1,448 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 User Groups crud controller for pylons
23 23 """
24 24
25 25 import logging
26 26 import formencode
27 27
28 28 import peppercorn
29 29 from formencode import htmlfill
30 30 from pylons import request, tmpl_context as c, url, config
31 31 from pylons.controllers.util import redirect
32 32 from pylons.i18n.translation import _
33 33
34 34 from sqlalchemy.orm import joinedload
35 35
36 36 from rhodecode.lib import auth
37 37 from rhodecode.lib import helpers as h
38 38 from rhodecode.lib import audit_logger
39 39 from rhodecode.lib.ext_json import json
40 40 from rhodecode.lib.exceptions import UserGroupAssignedException,\
41 41 RepoGroupAssignmentError
42 42 from rhodecode.lib.utils import jsonify
43 43 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
44 44 from rhodecode.lib.auth import (
45 45 LoginRequired, NotAnonymous, HasUserGroupPermissionAnyDecorator,
46 46 HasPermissionAnyDecorator, XHRRequired)
47 47 from rhodecode.lib.base import BaseController, render
48 48 from rhodecode.model.permission import PermissionModel
49 49 from rhodecode.model.scm import UserGroupList
50 50 from rhodecode.model.user_group import UserGroupModel
51 51 from rhodecode.model.db import (
52 52 User, UserGroup, UserGroupRepoToPerm, UserGroupRepoGroupToPerm)
53 53 from rhodecode.model.forms import (
54 54 UserGroupForm, UserGroupPermsForm, UserIndividualPermissionsForm,
55 55 UserPermissionsForm)
56 56 from rhodecode.model.meta import Session
57 57
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61
62 62 class UserGroupsController(BaseController):
63 63 """REST Controller styled on the Atom Publishing Protocol"""
64 64
65 65 @LoginRequired()
66 66 def __before__(self):
67 67 super(UserGroupsController, self).__before__()
68 68 c.available_permissions = config['available_permissions']
69 69 PermissionModel().set_global_permission_choices(c, gettext_translator=_)
70 70
71 71 def __load_data(self, user_group_id):
72 72 c.group_members_obj = [x.user for x in c.user_group.members]
73 73 c.group_members_obj.sort(key=lambda u: u.username.lower())
74 74 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
75 75
76 76 def __load_defaults(self, user_group_id):
77 77 """
78 78 Load defaults settings for edit, and update
79 79
80 80 :param user_group_id:
81 81 """
82 82 user_group = UserGroup.get_or_404(user_group_id)
83 83 data = user_group.get_dict()
84 84 # fill owner
85 85 if user_group.user:
86 86 data.update({'user': user_group.user.username})
87 87 else:
88 88 replacement_user = User.get_first_super_admin().username
89 89 data.update({'user': replacement_user})
90 90 return data
91 91
92 92 def _revoke_perms_on_yourself(self, form_result):
93 93 _updates = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
94 94 form_result['perm_updates'])
95 95 _additions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
96 96 form_result['perm_additions'])
97 97 _deletions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
98 98 form_result['perm_deletions'])
99 99 admin_perm = 'usergroup.admin'
100 100 if _updates and _updates[0][1] != admin_perm or \
101 101 _additions and _additions[0][1] != admin_perm or \
102 102 _deletions and _deletions[0][1] != admin_perm:
103 103 return True
104 104 return False
105 105
106 # permission check inside
107 @NotAnonymous()
108 def index(self):
109 # TODO(marcink): remove bind to self.request after pyramid migration
110 self.request = c.pyramid_request
111 _render = self.request.get_partial_renderer(
112 'data_table/_dt_elements.mako')
113
114 def user_group_name(user_group_id, user_group_name):
115 return _render("user_group_name", user_group_id, user_group_name)
116
117 def user_group_actions(user_group_id, user_group_name):
118 return _render("user_group_actions", user_group_id, user_group_name)
119
120 # json generate
121 group_iter = UserGroupList(UserGroup.query().all(),
122 perm_set=['usergroup.admin'])
123
124 user_groups_data = []
125 for user_gr in group_iter:
126 user_groups_data.append({
127 "group_name": user_group_name(
128 user_gr.users_group_id, h.escape(user_gr.users_group_name)),
129 "group_name_raw": user_gr.users_group_name,
130 "desc": h.escape(user_gr.user_group_description),
131 "members": len(user_gr.members),
132 "sync": user_gr.group_data.get('extern_type'),
133 "active": h.bool2icon(user_gr.users_group_active),
134 "owner": h.escape(h.link_to_user(user_gr.user.username)),
135 "action": user_group_actions(
136 user_gr.users_group_id, user_gr.users_group_name)
137 })
138
139 c.data = json.dumps(user_groups_data)
140 return render('admin/user_groups/user_groups.mako')
141
142 106 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
143 107 @auth.CSRFRequired()
144 108 def create(self):
145 109
146 110 users_group_form = UserGroupForm()()
147 111 try:
148 112 form_result = users_group_form.to_python(dict(request.POST))
149 113 user_group = UserGroupModel().create(
150 114 name=form_result['users_group_name'],
151 115 description=form_result['user_group_description'],
152 116 owner=c.rhodecode_user.user_id,
153 117 active=form_result['users_group_active'])
154 118 Session().flush()
155 119 creation_data = user_group.get_api_data()
156 120 user_group_name = form_result['users_group_name']
157 121
158 122 audit_logger.store_web(
159 123 'user_group.create', action_data={'data': creation_data},
160 124 user=c.rhodecode_user)
161 125
162 126 user_group_link = h.link_to(
163 127 h.escape(user_group_name),
164 128 url('edit_users_group', user_group_id=user_group.users_group_id))
165 129 h.flash(h.literal(_('Created user group %(user_group_link)s')
166 130 % {'user_group_link': user_group_link}),
167 131 category='success')
168 132 Session().commit()
169 133 except formencode.Invalid as errors:
170 134 return htmlfill.render(
171 135 render('admin/user_groups/user_group_add.mako'),
172 136 defaults=errors.value,
173 137 errors=errors.error_dict or {},
174 138 prefix_error=False,
175 139 encoding="UTF-8",
176 140 force_defaults=False)
177 141 except Exception:
178 142 log.exception("Exception creating user group")
179 143 h.flash(_('Error occurred during creation of user group %s') \
180 144 % request.POST.get('users_group_name'), category='error')
181 145
182 146 return redirect(
183 147 url('edit_users_group', user_group_id=user_group.users_group_id))
184 148
185 149 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
186 150 def new(self):
187 151 """GET /user_groups/new: Form to create a new item"""
188 152 # url('new_users_group')
189 153 return render('admin/user_groups/user_group_add.mako')
190 154
191 155 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
192 156 @auth.CSRFRequired()
193 157 def update(self, user_group_id):
194 158
195 159 user_group_id = safe_int(user_group_id)
196 160 c.user_group = UserGroup.get_or_404(user_group_id)
197 161 c.active = 'settings'
198 162 self.__load_data(user_group_id)
199 163
200 164 users_group_form = UserGroupForm(
201 165 edit=True, old_data=c.user_group.get_dict(), allow_disabled=True)()
202 166
203 167 old_values = c.user_group.get_api_data()
204 168 try:
205 169 form_result = users_group_form.to_python(request.POST)
206 170 pstruct = peppercorn.parse(request.POST.items())
207 171 form_result['users_group_members'] = pstruct['user_group_members']
208 172
209 173 user_group, added_members, removed_members = \
210 174 UserGroupModel().update(c.user_group, form_result)
211 175 updated_user_group = form_result['users_group_name']
212 176
213 177 audit_logger.store_web(
214 178 'user_group.edit', action_data={'old_data': old_values},
215 179 user=c.rhodecode_user)
216 180
217 181 # TODO(marcink): use added/removed to set user_group.edit.member.add
218 182
219 183 h.flash(_('Updated user group %s') % updated_user_group,
220 184 category='success')
221 185 Session().commit()
222 186 except formencode.Invalid as errors:
223 187 defaults = errors.value
224 188 e = errors.error_dict or {}
225 189
226 190 return htmlfill.render(
227 191 render('admin/user_groups/user_group_edit.mako'),
228 192 defaults=defaults,
229 193 errors=e,
230 194 prefix_error=False,
231 195 encoding="UTF-8",
232 196 force_defaults=False)
233 197 except Exception:
234 198 log.exception("Exception during update of user group")
235 199 h.flash(_('Error occurred during update of user group %s')
236 200 % request.POST.get('users_group_name'), category='error')
237 201
238 202 return redirect(url('edit_users_group', user_group_id=user_group_id))
239 203
240 204 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
241 205 @auth.CSRFRequired()
242 206 def delete(self, user_group_id):
243 207 user_group_id = safe_int(user_group_id)
244 208 c.user_group = UserGroup.get_or_404(user_group_id)
245 209 force = str2bool(request.POST.get('force'))
246 210
247 211 old_values = c.user_group.get_api_data()
248 212 try:
249 213 UserGroupModel().delete(c.user_group, force=force)
250 214 audit_logger.store_web(
251 215 'user.delete', action_data={'old_data': old_values},
252 216 user=c.rhodecode_user)
253 217 Session().commit()
254 218 h.flash(_('Successfully deleted user group'), category='success')
255 219 except UserGroupAssignedException as e:
256 220 h.flash(str(e), category='error')
257 221 except Exception:
258 222 log.exception("Exception during deletion of user group")
259 223 h.flash(_('An error occurred during deletion of user group'),
260 224 category='error')
261 225 return redirect(url('users_groups'))
262 226
263 227 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
264 228 def edit(self, user_group_id):
265 229 """GET /user_groups/user_group_id/edit: Form to edit an existing item"""
266 230 # url('edit_users_group', user_group_id=ID)
267 231
268 232 user_group_id = safe_int(user_group_id)
269 233 c.user_group = UserGroup.get_or_404(user_group_id)
270 234 c.active = 'settings'
271 235 self.__load_data(user_group_id)
272 236
273 237 defaults = self.__load_defaults(user_group_id)
274 238
275 239 return htmlfill.render(
276 240 render('admin/user_groups/user_group_edit.mako'),
277 241 defaults=defaults,
278 242 encoding="UTF-8",
279 243 force_defaults=False
280 244 )
281 245
282 246 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
283 247 def edit_perms(self, user_group_id):
284 248 user_group_id = safe_int(user_group_id)
285 249 c.user_group = UserGroup.get_or_404(user_group_id)
286 250 c.active = 'perms'
287 251
288 252 defaults = {}
289 253 # fill user group users
290 254 for p in c.user_group.user_user_group_to_perm:
291 255 defaults.update({'u_perm_%s' % p.user.user_id:
292 256 p.permission.permission_name})
293 257
294 258 for p in c.user_group.user_group_user_group_to_perm:
295 259 defaults.update({'g_perm_%s' % p.user_group.users_group_id:
296 260 p.permission.permission_name})
297 261
298 262 return htmlfill.render(
299 263 render('admin/user_groups/user_group_edit.mako'),
300 264 defaults=defaults,
301 265 encoding="UTF-8",
302 266 force_defaults=False
303 267 )
304 268
305 269 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
306 270 @auth.CSRFRequired()
307 271 def update_perms(self, user_group_id):
308 272 """
309 273 grant permission for given usergroup
310 274
311 275 :param user_group_id:
312 276 """
313 277 user_group_id = safe_int(user_group_id)
314 278 c.user_group = UserGroup.get_or_404(user_group_id)
315 279 form = UserGroupPermsForm()().to_python(request.POST)
316 280
317 281 if not c.rhodecode_user.is_admin:
318 282 if self._revoke_perms_on_yourself(form):
319 283 msg = _('Cannot change permission for yourself as admin')
320 284 h.flash(msg, category='warning')
321 285 return redirect(url('edit_user_group_perms', user_group_id=user_group_id))
322 286
323 287 try:
324 288 UserGroupModel().update_permissions(user_group_id,
325 289 form['perm_additions'], form['perm_updates'], form['perm_deletions'])
326 290 except RepoGroupAssignmentError:
327 291 h.flash(_('Target group cannot be the same'), category='error')
328 292 return redirect(url('edit_user_group_perms', user_group_id=user_group_id))
329 293
330 294 # TODO(marcink): implement global permissions
331 295 # audit_log.store_web('user_group.edit.permissions')
332 296 Session().commit()
333 297 h.flash(_('User Group permissions updated'), category='success')
334 298 return redirect(url('edit_user_group_perms', user_group_id=user_group_id))
335 299
336 300 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
337 301 def edit_perms_summary(self, user_group_id):
338 302 user_group_id = safe_int(user_group_id)
339 303 c.user_group = UserGroup.get_or_404(user_group_id)
340 304 c.active = 'perms_summary'
341 305 permissions = {
342 306 'repositories': {},
343 307 'repositories_groups': {},
344 308 }
345 309 ugroup_repo_perms = UserGroupRepoToPerm.query()\
346 310 .options(joinedload(UserGroupRepoToPerm.permission))\
347 311 .options(joinedload(UserGroupRepoToPerm.repository))\
348 312 .filter(UserGroupRepoToPerm.users_group_id == user_group_id)\
349 313 .all()
350 314
351 315 for gr in ugroup_repo_perms:
352 316 permissions['repositories'][gr.repository.repo_name] \
353 317 = gr.permission.permission_name
354 318
355 319 ugroup_group_perms = UserGroupRepoGroupToPerm.query()\
356 320 .options(joinedload(UserGroupRepoGroupToPerm.permission))\
357 321 .options(joinedload(UserGroupRepoGroupToPerm.group))\
358 322 .filter(UserGroupRepoGroupToPerm.users_group_id == user_group_id)\
359 323 .all()
360 324
361 325 for gr in ugroup_group_perms:
362 326 permissions['repositories_groups'][gr.group.group_name] \
363 327 = gr.permission.permission_name
364 328 c.permissions = permissions
365 329 return render('admin/user_groups/user_group_edit.mako')
366 330
367 331 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
368 332 def edit_global_perms(self, user_group_id):
369 333 user_group_id = safe_int(user_group_id)
370 334 c.user_group = UserGroup.get_or_404(user_group_id)
371 335 c.active = 'global_perms'
372 336
373 337 c.default_user = User.get_default_user()
374 338 defaults = c.user_group.get_dict()
375 339 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
376 340 defaults.update(c.user_group.get_default_perms())
377 341
378 342 return htmlfill.render(
379 343 render('admin/user_groups/user_group_edit.mako'),
380 344 defaults=defaults,
381 345 encoding="UTF-8",
382 346 force_defaults=False
383 347 )
384 348
385 349 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
386 350 @auth.CSRFRequired()
387 351 def update_global_perms(self, user_group_id):
388 352 user_group_id = safe_int(user_group_id)
389 353 user_group = UserGroup.get_or_404(user_group_id)
390 354 c.active = 'global_perms'
391 355
392 356 try:
393 357 # first stage that verifies the checkbox
394 358 _form = UserIndividualPermissionsForm()
395 359 form_result = _form.to_python(dict(request.POST))
396 360 inherit_perms = form_result['inherit_default_permissions']
397 361 user_group.inherit_default_permissions = inherit_perms
398 362 Session().add(user_group)
399 363
400 364 if not inherit_perms:
401 365 # only update the individual ones if we un check the flag
402 366 _form = UserPermissionsForm(
403 367 [x[0] for x in c.repo_create_choices],
404 368 [x[0] for x in c.repo_create_on_write_choices],
405 369 [x[0] for x in c.repo_group_create_choices],
406 370 [x[0] for x in c.user_group_create_choices],
407 371 [x[0] for x in c.fork_choices],
408 372 [x[0] for x in c.inherit_default_permission_choices])()
409 373
410 374 form_result = _form.to_python(dict(request.POST))
411 375 form_result.update({'perm_user_group_id': user_group.users_group_id})
412 376
413 377 PermissionModel().update_user_group_permissions(form_result)
414 378
415 379 Session().commit()
416 380 h.flash(_('User Group global permissions updated successfully'),
417 381 category='success')
418 382
419 383 except formencode.Invalid as errors:
420 384 defaults = errors.value
421 385 c.user_group = user_group
422 386 return htmlfill.render(
423 387 render('admin/user_groups/user_group_edit.mako'),
424 388 defaults=defaults,
425 389 errors=errors.error_dict or {},
426 390 prefix_error=False,
427 391 encoding="UTF-8",
428 392 force_defaults=False)
429 393 except Exception:
430 394 log.exception("Exception during permissions saving")
431 395 h.flash(_('An error occurred during permissions saving'),
432 396 category='error')
433 397
434 398 return redirect(url('edit_user_group_global_perms', user_group_id=user_group_id))
435 399
436 400 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
437 401 def edit_advanced(self, user_group_id):
438 402 user_group_id = safe_int(user_group_id)
439 403 c.user_group = UserGroup.get_or_404(user_group_id)
440 404 c.active = 'advanced'
441 405 c.group_members_obj = sorted(
442 406 (x.user for x in c.user_group.members),
443 407 key=lambda u: u.username.lower())
444 408
445 409 c.group_to_repos = sorted(
446 410 (x.repository for x in c.user_group.users_group_repo_to_perm),
447 411 key=lambda u: u.repo_name.lower())
448 412
449 413 c.group_to_repo_groups = sorted(
450 414 (x.group for x in c.user_group.users_group_repo_group_to_perm),
451 415 key=lambda u: u.group_name.lower())
452 416
453 417 return render('admin/user_groups/user_group_edit.mako')
454 418
455 419 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
456 420 def edit_advanced_set_synchronization(self, user_group_id):
457 421 user_group_id = safe_int(user_group_id)
458 422 user_group = UserGroup.get_or_404(user_group_id)
459 423
460 424 existing = user_group.group_data.get('extern_type')
461 425
462 426 if existing:
463 427 new_state = user_group.group_data
464 428 new_state['extern_type'] = None
465 429 else:
466 430 new_state = user_group.group_data
467 431 new_state['extern_type'] = 'manual'
468 432 new_state['extern_type_set_by'] = c.rhodecode_user.username
469 433
470 434 try:
471 435 user_group.group_data = new_state
472 436 Session().add(user_group)
473 437 Session().commit()
474 438
475 439 h.flash(_('User Group synchronization updated successfully'),
476 440 category='success')
477 441 except Exception:
478 442 log.exception("Exception during sync settings saving")
479 443 h.flash(_('An error occurred during synchronization update'),
480 444 category='error')
481 445
482 446 return redirect(
483 447 url('edit_user_group_advanced', user_group_id=user_group_id))
484 448
485 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
486 @XHRRequired()
487 @jsonify
488 def user_group_members(self, user_group_id):
489 """
490 Return members of given user group
491 """
492 user_group_id = safe_int(user_group_id)
493 user_group = UserGroup.get_or_404(user_group_id)
494 group_members_obj = sorted((x.user for x in user_group.members),
495 key=lambda u: u.username.lower())
496
497 group_members = [
498 {
499 'id': user.user_id,
500 'first_name': user.first_name,
501 'last_name': user.last_name,
502 'username': user.username,
503 'icon_link': h.gravatar_url(user.email, 30),
504 'value_display': h.person(user.email),
505 'value': user.username,
506 'value_type': 'user',
507 'active': user.active,
508 }
509 for user in group_members_obj
510 ]
511
512 return {
513 'members': group_members
514 }
@@ -1,982 +1,982 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Utilities library for RhodeCode
23 23 """
24 24
25 25 import datetime
26 26 import decorator
27 27 import json
28 28 import logging
29 29 import os
30 30 import re
31 31 import shutil
32 32 import tempfile
33 33 import traceback
34 34 import tarfile
35 35 import warnings
36 36 import hashlib
37 37 from os.path import join as jn
38 38
39 39 import paste
40 40 import pkg_resources
41 41 from paste.script.command import Command, BadCommand
42 42 from webhelpers.text import collapse, remove_formatting, strip_tags
43 43 from mako import exceptions
44 44 from pyramid.threadlocal import get_current_registry
45 45 from pyramid.request import Request
46 46
47 47 from rhodecode.lib.fakemod import create_module
48 48 from rhodecode.lib.vcs.backends.base import Config
49 49 from rhodecode.lib.vcs.exceptions import VCSError
50 50 from rhodecode.lib.vcs.utils.helpers import get_scm, get_scm_backend
51 51 from rhodecode.lib.utils2 import (
52 52 safe_str, safe_unicode, get_current_rhodecode_user, md5)
53 53 from rhodecode.model import meta
54 54 from rhodecode.model.db import (
55 55 Repository, User, RhodeCodeUi, UserLog, RepoGroup, UserGroup)
56 56 from rhodecode.model.meta import Session
57 57
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
62 62
63 63 # String which contains characters that are not allowed in slug names for
64 64 # repositories or repository groups. It is properly escaped to use it in
65 65 # regular expressions.
66 66 SLUG_BAD_CHARS = re.escape('`?=[]\;\'"<>,/~!@#$%^&*()+{}|:')
67 67
68 68 # Regex that matches forbidden characters in repo/group slugs.
69 69 SLUG_BAD_CHAR_RE = re.compile('[{}]'.format(SLUG_BAD_CHARS))
70 70
71 71 # Regex that matches allowed characters in repo/group slugs.
72 72 SLUG_GOOD_CHAR_RE = re.compile('[^{}]'.format(SLUG_BAD_CHARS))
73 73
74 74 # Regex that matches whole repo/group slugs.
75 75 SLUG_RE = re.compile('[^{}]+'.format(SLUG_BAD_CHARS))
76 76
77 77 _license_cache = None
78 78
79 79
80 80 def repo_name_slug(value):
81 81 """
82 82 Return slug of name of repository
83 83 This function is called on each creation/modification
84 84 of repository to prevent bad names in repo
85 85 """
86 86 replacement_char = '-'
87 87
88 88 slug = remove_formatting(value)
89 89 slug = SLUG_BAD_CHAR_RE.sub('', slug)
90 90 slug = re.sub('[\s]+', '-', slug)
91 91 slug = collapse(slug, replacement_char)
92 92 return slug
93 93
94 94
95 95 #==============================================================================
96 96 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
97 97 #==============================================================================
98 98 def get_repo_slug(request):
99 99 if isinstance(request, Request) and getattr(request, 'db_repo', None):
100 100 # pyramid
101 101 _repo = request.db_repo.repo_name
102 102 else:
103 103 # TODO(marcink): remove after pylons migration...
104 104 _repo = request.environ['pylons.routes_dict'].get('repo_name')
105 105
106 106 if _repo:
107 107 _repo = _repo.rstrip('/')
108 108 return _repo
109 109
110 110
111 111 def get_repo_group_slug(request):
112 112 if isinstance(request, Request) and getattr(request, 'matchdict', None):
113 113 # pyramid
114 114 _group = request.matchdict.get('repo_group_name')
115 115 else:
116 116 _group = request.environ['pylons.routes_dict'].get('group_name')
117 117
118 118 if _group:
119 119 _group = _group.rstrip('/')
120 120 return _group
121 121
122 122
123 123 def get_user_group_slug(request):
124 124 if isinstance(request, Request) and getattr(request, 'matchdict', None):
125 125 # pyramid
126 126 _group = request.matchdict.get('user_group_id')
127 127 else:
128 128 _group = request.environ['pylons.routes_dict'].get('user_group_id')
129 129
130 130 try:
131 131 _group = UserGroup.get(_group)
132 132 if _group:
133 133 _group = _group.users_group_name
134 134 except Exception:
135 log.debug(traceback.format_exc())
135 log.exception('Failed to get user group by id')
136 136 # catch all failures here
137 pass
137 return None
138 138
139 139 return _group
140 140
141 141
142 142 def get_filesystem_repos(path, recursive=False, skip_removed_repos=True):
143 143 """
144 144 Scans given path for repos and return (name,(type,path)) tuple
145 145
146 146 :param path: path to scan for repositories
147 147 :param recursive: recursive search and return names with subdirs in front
148 148 """
149 149
150 150 # remove ending slash for better results
151 151 path = path.rstrip(os.sep)
152 152 log.debug('now scanning in %s location recursive:%s...', path, recursive)
153 153
154 154 def _get_repos(p):
155 155 dirpaths = _get_dirpaths(p)
156 156 if not _is_dir_writable(p):
157 157 log.warning('repo path without write access: %s', p)
158 158
159 159 for dirpath in dirpaths:
160 160 if os.path.isfile(os.path.join(p, dirpath)):
161 161 continue
162 162 cur_path = os.path.join(p, dirpath)
163 163
164 164 # skip removed repos
165 165 if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
166 166 continue
167 167
168 168 #skip .<somethin> dirs
169 169 if dirpath.startswith('.'):
170 170 continue
171 171
172 172 try:
173 173 scm_info = get_scm(cur_path)
174 174 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
175 175 except VCSError:
176 176 if not recursive:
177 177 continue
178 178 #check if this dir containts other repos for recursive scan
179 179 rec_path = os.path.join(p, dirpath)
180 180 if os.path.isdir(rec_path):
181 181 for inner_scm in _get_repos(rec_path):
182 182 yield inner_scm
183 183
184 184 return _get_repos(path)
185 185
186 186
187 187 def _get_dirpaths(p):
188 188 try:
189 189 # OS-independable way of checking if we have at least read-only
190 190 # access or not.
191 191 dirpaths = os.listdir(p)
192 192 except OSError:
193 193 log.warning('ignoring repo path without read access: %s', p)
194 194 return []
195 195
196 196 # os.listpath has a tweak: If a unicode is passed into it, then it tries to
197 197 # decode paths and suddenly returns unicode objects itself. The items it
198 198 # cannot decode are returned as strings and cause issues.
199 199 #
200 200 # Those paths are ignored here until a solid solution for path handling has
201 201 # been built.
202 202 expected_type = type(p)
203 203
204 204 def _has_correct_type(item):
205 205 if type(item) is not expected_type:
206 206 log.error(
207 207 u"Ignoring path %s since it cannot be decoded into unicode.",
208 208 # Using "repr" to make sure that we see the byte value in case
209 209 # of support.
210 210 repr(item))
211 211 return False
212 212 return True
213 213
214 214 dirpaths = [item for item in dirpaths if _has_correct_type(item)]
215 215
216 216 return dirpaths
217 217
218 218
219 219 def _is_dir_writable(path):
220 220 """
221 221 Probe if `path` is writable.
222 222
223 223 Due to trouble on Cygwin / Windows, this is actually probing if it is
224 224 possible to create a file inside of `path`, stat does not produce reliable
225 225 results in this case.
226 226 """
227 227 try:
228 228 with tempfile.TemporaryFile(dir=path):
229 229 pass
230 230 except OSError:
231 231 return False
232 232 return True
233 233
234 234
235 235 def is_valid_repo(repo_name, base_path, expect_scm=None, explicit_scm=None):
236 236 """
237 237 Returns True if given path is a valid repository False otherwise.
238 238 If expect_scm param is given also, compare if given scm is the same
239 239 as expected from scm parameter. If explicit_scm is given don't try to
240 240 detect the scm, just use the given one to check if repo is valid
241 241
242 242 :param repo_name:
243 243 :param base_path:
244 244 :param expect_scm:
245 245 :param explicit_scm:
246 246
247 247 :return True: if given path is a valid repository
248 248 """
249 249 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
250 250 log.debug('Checking if `%s` is a valid path for repository. '
251 251 'Explicit type: %s', repo_name, explicit_scm)
252 252
253 253 try:
254 254 if explicit_scm:
255 255 detected_scms = [get_scm_backend(explicit_scm)]
256 256 else:
257 257 detected_scms = get_scm(full_path)
258 258
259 259 if expect_scm:
260 260 return detected_scms[0] == expect_scm
261 261 log.debug('path: %s is an vcs object:%s', full_path, detected_scms)
262 262 return True
263 263 except VCSError:
264 264 log.debug('path: %s is not a valid repo !', full_path)
265 265 return False
266 266
267 267
268 268 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
269 269 """
270 270 Returns True if given path is a repository group, False otherwise
271 271
272 272 :param repo_name:
273 273 :param base_path:
274 274 """
275 275 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
276 276 log.debug('Checking if `%s` is a valid path for repository group',
277 277 repo_group_name)
278 278
279 279 # check if it's not a repo
280 280 if is_valid_repo(repo_group_name, base_path):
281 281 log.debug('Repo called %s exist, it is not a valid '
282 282 'repo group' % repo_group_name)
283 283 return False
284 284
285 285 try:
286 286 # we need to check bare git repos at higher level
287 287 # since we might match branches/hooks/info/objects or possible
288 288 # other things inside bare git repo
289 289 scm_ = get_scm(os.path.dirname(full_path))
290 290 log.debug('path: %s is a vcs object:%s, not valid '
291 291 'repo group' % (full_path, scm_))
292 292 return False
293 293 except VCSError:
294 294 pass
295 295
296 296 # check if it's a valid path
297 297 if skip_path_check or os.path.isdir(full_path):
298 298 log.debug('path: %s is a valid repo group !', full_path)
299 299 return True
300 300
301 301 log.debug('path: %s is not a valid repo group !', full_path)
302 302 return False
303 303
304 304
305 305 def ask_ok(prompt, retries=4, complaint='[y]es or [n]o please!'):
306 306 while True:
307 307 ok = raw_input(prompt)
308 308 if ok.lower() in ('y', 'ye', 'yes'):
309 309 return True
310 310 if ok.lower() in ('n', 'no', 'nop', 'nope'):
311 311 return False
312 312 retries = retries - 1
313 313 if retries < 0:
314 314 raise IOError
315 315 print(complaint)
316 316
317 317 # propagated from mercurial documentation
318 318 ui_sections = [
319 319 'alias', 'auth',
320 320 'decode/encode', 'defaults',
321 321 'diff', 'email',
322 322 'extensions', 'format',
323 323 'merge-patterns', 'merge-tools',
324 324 'hooks', 'http_proxy',
325 325 'smtp', 'patch',
326 326 'paths', 'profiling',
327 327 'server', 'trusted',
328 328 'ui', 'web', ]
329 329
330 330
331 331 def config_data_from_db(clear_session=True, repo=None):
332 332 """
333 333 Read the configuration data from the database and return configuration
334 334 tuples.
335 335 """
336 336 from rhodecode.model.settings import VcsSettingsModel
337 337
338 338 config = []
339 339
340 340 sa = meta.Session()
341 341 settings_model = VcsSettingsModel(repo=repo, sa=sa)
342 342
343 343 ui_settings = settings_model.get_ui_settings()
344 344
345 345 for setting in ui_settings:
346 346 if setting.active:
347 347 log.debug(
348 348 'settings ui from db: [%s] %s=%s',
349 349 setting.section, setting.key, setting.value)
350 350 config.append((
351 351 safe_str(setting.section), safe_str(setting.key),
352 352 safe_str(setting.value)))
353 353 if setting.key == 'push_ssl':
354 354 # force set push_ssl requirement to False, rhodecode
355 355 # handles that
356 356 config.append((
357 357 safe_str(setting.section), safe_str(setting.key), False))
358 358 if clear_session:
359 359 meta.Session.remove()
360 360
361 361 # TODO: mikhail: probably it makes no sense to re-read hooks information.
362 362 # It's already there and activated/deactivated
363 363 skip_entries = []
364 364 enabled_hook_classes = get_enabled_hook_classes(ui_settings)
365 365 if 'pull' not in enabled_hook_classes:
366 366 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PULL))
367 367 if 'push' not in enabled_hook_classes:
368 368 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PUSH))
369 369 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRETX_PUSH))
370 370 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PUSH_KEY))
371 371
372 372 config = [entry for entry in config if entry[:2] not in skip_entries]
373 373
374 374 return config
375 375
376 376
377 377 def make_db_config(clear_session=True, repo=None):
378 378 """
379 379 Create a :class:`Config` instance based on the values in the database.
380 380 """
381 381 config = Config()
382 382 config_data = config_data_from_db(clear_session=clear_session, repo=repo)
383 383 for section, option, value in config_data:
384 384 config.set(section, option, value)
385 385 return config
386 386
387 387
388 388 def get_enabled_hook_classes(ui_settings):
389 389 """
390 390 Return the enabled hook classes.
391 391
392 392 :param ui_settings: List of ui_settings as returned
393 393 by :meth:`VcsSettingsModel.get_ui_settings`
394 394
395 395 :return: a list with the enabled hook classes. The order is not guaranteed.
396 396 :rtype: list
397 397 """
398 398 enabled_hooks = []
399 399 active_hook_keys = [
400 400 key for section, key, value, active in ui_settings
401 401 if section == 'hooks' and active]
402 402
403 403 hook_names = {
404 404 RhodeCodeUi.HOOK_PUSH: 'push',
405 405 RhodeCodeUi.HOOK_PULL: 'pull',
406 406 RhodeCodeUi.HOOK_REPO_SIZE: 'repo_size'
407 407 }
408 408
409 409 for key in active_hook_keys:
410 410 hook = hook_names.get(key)
411 411 if hook:
412 412 enabled_hooks.append(hook)
413 413
414 414 return enabled_hooks
415 415
416 416
417 417 def set_rhodecode_config(config):
418 418 """
419 419 Updates pylons config with new settings from database
420 420
421 421 :param config:
422 422 """
423 423 from rhodecode.model.settings import SettingsModel
424 424 app_settings = SettingsModel().get_all_settings()
425 425
426 426 for k, v in app_settings.items():
427 427 config[k] = v
428 428
429 429
430 430 def get_rhodecode_realm():
431 431 """
432 432 Return the rhodecode realm from database.
433 433 """
434 434 from rhodecode.model.settings import SettingsModel
435 435 realm = SettingsModel().get_setting_by_name('realm')
436 436 return safe_str(realm.app_settings_value)
437 437
438 438
439 439 def get_rhodecode_base_path():
440 440 """
441 441 Returns the base path. The base path is the filesystem path which points
442 442 to the repository store.
443 443 """
444 444 from rhodecode.model.settings import SettingsModel
445 445 paths_ui = SettingsModel().get_ui_by_section_and_key('paths', '/')
446 446 return safe_str(paths_ui.ui_value)
447 447
448 448
449 449 def map_groups(path):
450 450 """
451 451 Given a full path to a repository, create all nested groups that this
452 452 repo is inside. This function creates parent-child relationships between
453 453 groups and creates default perms for all new groups.
454 454
455 455 :param paths: full path to repository
456 456 """
457 457 from rhodecode.model.repo_group import RepoGroupModel
458 458 sa = meta.Session()
459 459 groups = path.split(Repository.NAME_SEP)
460 460 parent = None
461 461 group = None
462 462
463 463 # last element is repo in nested groups structure
464 464 groups = groups[:-1]
465 465 rgm = RepoGroupModel(sa)
466 466 owner = User.get_first_super_admin()
467 467 for lvl, group_name in enumerate(groups):
468 468 group_name = '/'.join(groups[:lvl] + [group_name])
469 469 group = RepoGroup.get_by_group_name(group_name)
470 470 desc = '%s group' % group_name
471 471
472 472 # skip folders that are now removed repos
473 473 if REMOVED_REPO_PAT.match(group_name):
474 474 break
475 475
476 476 if group is None:
477 477 log.debug('creating group level: %s group_name: %s',
478 478 lvl, group_name)
479 479 group = RepoGroup(group_name, parent)
480 480 group.group_description = desc
481 481 group.user = owner
482 482 sa.add(group)
483 483 perm_obj = rgm._create_default_perms(group)
484 484 sa.add(perm_obj)
485 485 sa.flush()
486 486
487 487 parent = group
488 488 return group
489 489
490 490
491 491 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
492 492 """
493 493 maps all repos given in initial_repo_list, non existing repositories
494 494 are created, if remove_obsolete is True it also checks for db entries
495 495 that are not in initial_repo_list and removes them.
496 496
497 497 :param initial_repo_list: list of repositories found by scanning methods
498 498 :param remove_obsolete: check for obsolete entries in database
499 499 """
500 500 from rhodecode.model.repo import RepoModel
501 501 from rhodecode.model.scm import ScmModel
502 502 from rhodecode.model.repo_group import RepoGroupModel
503 503 from rhodecode.model.settings import SettingsModel
504 504
505 505 sa = meta.Session()
506 506 repo_model = RepoModel()
507 507 user = User.get_first_super_admin()
508 508 added = []
509 509
510 510 # creation defaults
511 511 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
512 512 enable_statistics = defs.get('repo_enable_statistics')
513 513 enable_locking = defs.get('repo_enable_locking')
514 514 enable_downloads = defs.get('repo_enable_downloads')
515 515 private = defs.get('repo_private')
516 516
517 517 for name, repo in initial_repo_list.items():
518 518 group = map_groups(name)
519 519 unicode_name = safe_unicode(name)
520 520 db_repo = repo_model.get_by_repo_name(unicode_name)
521 521 # found repo that is on filesystem not in RhodeCode database
522 522 if not db_repo:
523 523 log.info('repository %s not found, creating now', name)
524 524 added.append(name)
525 525 desc = (repo.description
526 526 if repo.description != 'unknown'
527 527 else '%s repository' % name)
528 528
529 529 db_repo = repo_model._create_repo(
530 530 repo_name=name,
531 531 repo_type=repo.alias,
532 532 description=desc,
533 533 repo_group=getattr(group, 'group_id', None),
534 534 owner=user,
535 535 enable_locking=enable_locking,
536 536 enable_downloads=enable_downloads,
537 537 enable_statistics=enable_statistics,
538 538 private=private,
539 539 state=Repository.STATE_CREATED
540 540 )
541 541 sa.commit()
542 542 # we added that repo just now, and make sure we updated server info
543 543 if db_repo.repo_type == 'git':
544 544 git_repo = db_repo.scm_instance()
545 545 # update repository server-info
546 546 log.debug('Running update server info')
547 547 git_repo._update_server_info()
548 548
549 549 db_repo.update_commit_cache()
550 550
551 551 config = db_repo._config
552 552 config.set('extensions', 'largefiles', '')
553 553 ScmModel().install_hooks(
554 554 db_repo.scm_instance(config=config),
555 555 repo_type=db_repo.repo_type)
556 556
557 557 removed = []
558 558 if remove_obsolete:
559 559 # remove from database those repositories that are not in the filesystem
560 560 for repo in sa.query(Repository).all():
561 561 if repo.repo_name not in initial_repo_list.keys():
562 562 log.debug("Removing non-existing repository found in db `%s`",
563 563 repo.repo_name)
564 564 try:
565 565 RepoModel(sa).delete(repo, forks='detach', fs_remove=False)
566 566 sa.commit()
567 567 removed.append(repo.repo_name)
568 568 except Exception:
569 569 # don't hold further removals on error
570 570 log.error(traceback.format_exc())
571 571 sa.rollback()
572 572
573 573 def splitter(full_repo_name):
574 574 _parts = full_repo_name.rsplit(RepoGroup.url_sep(), 1)
575 575 gr_name = None
576 576 if len(_parts) == 2:
577 577 gr_name = _parts[0]
578 578 return gr_name
579 579
580 580 initial_repo_group_list = [splitter(x) for x in
581 581 initial_repo_list.keys() if splitter(x)]
582 582
583 583 # remove from database those repository groups that are not in the
584 584 # filesystem due to parent child relationships we need to delete them
585 585 # in a specific order of most nested first
586 586 all_groups = [x.group_name for x in sa.query(RepoGroup).all()]
587 587 nested_sort = lambda gr: len(gr.split('/'))
588 588 for group_name in sorted(all_groups, key=nested_sort, reverse=True):
589 589 if group_name not in initial_repo_group_list:
590 590 repo_group = RepoGroup.get_by_group_name(group_name)
591 591 if (repo_group.children.all() or
592 592 not RepoGroupModel().check_exist_filesystem(
593 593 group_name=group_name, exc_on_failure=False)):
594 594 continue
595 595
596 596 log.info(
597 597 'Removing non-existing repository group found in db `%s`',
598 598 group_name)
599 599 try:
600 600 RepoGroupModel(sa).delete(group_name, fs_remove=False)
601 601 sa.commit()
602 602 removed.append(group_name)
603 603 except Exception:
604 604 # don't hold further removals on error
605 605 log.exception(
606 606 'Unable to remove repository group `%s`',
607 607 group_name)
608 608 sa.rollback()
609 609 raise
610 610
611 611 return added, removed
612 612
613 613
614 614 def get_default_cache_settings(settings):
615 615 cache_settings = {}
616 616 for key in settings.keys():
617 617 for prefix in ['beaker.cache.', 'cache.']:
618 618 if key.startswith(prefix):
619 619 name = key.split(prefix)[1].strip()
620 620 cache_settings[name] = settings[key].strip()
621 621 return cache_settings
622 622
623 623
624 624 # set cache regions for beaker so celery can utilise it
625 625 def add_cache(settings):
626 626 from rhodecode.lib import caches
627 627 cache_settings = {'regions': None}
628 628 # main cache settings used as default ...
629 629 cache_settings.update(get_default_cache_settings(settings))
630 630
631 631 if cache_settings['regions']:
632 632 for region in cache_settings['regions'].split(','):
633 633 region = region.strip()
634 634 region_settings = {}
635 635 for key, value in cache_settings.items():
636 636 if key.startswith(region):
637 637 region_settings[key.split('.')[1]] = value
638 638
639 639 caches.configure_cache_region(
640 640 region, region_settings, cache_settings)
641 641
642 642
643 643 def load_rcextensions(root_path):
644 644 import rhodecode
645 645 from rhodecode.config import conf
646 646
647 647 path = os.path.join(root_path, 'rcextensions', '__init__.py')
648 648 if os.path.isfile(path):
649 649 rcext = create_module('rc', path)
650 650 EXT = rhodecode.EXTENSIONS = rcext
651 651 log.debug('Found rcextensions now loading %s...', rcext)
652 652
653 653 # Additional mappings that are not present in the pygments lexers
654 654 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
655 655
656 656 # auto check if the module is not missing any data, set to default if is
657 657 # this will help autoupdate new feature of rcext module
658 658 #from rhodecode.config import rcextensions
659 659 #for k in dir(rcextensions):
660 660 # if not k.startswith('_') and not hasattr(EXT, k):
661 661 # setattr(EXT, k, getattr(rcextensions, k))
662 662
663 663
664 664 def get_custom_lexer(extension):
665 665 """
666 666 returns a custom lexer if it is defined in rcextensions module, or None
667 667 if there's no custom lexer defined
668 668 """
669 669 import rhodecode
670 670 from pygments import lexers
671 671
672 672 # custom override made by RhodeCode
673 673 if extension in ['mako']:
674 674 return lexers.get_lexer_by_name('html+mako')
675 675
676 676 # check if we didn't define this extension as other lexer
677 677 extensions = rhodecode.EXTENSIONS and getattr(rhodecode.EXTENSIONS, 'EXTRA_LEXERS', None)
678 678 if extensions and extension in rhodecode.EXTENSIONS.EXTRA_LEXERS:
679 679 _lexer_name = rhodecode.EXTENSIONS.EXTRA_LEXERS[extension]
680 680 return lexers.get_lexer_by_name(_lexer_name)
681 681
682 682
683 683 #==============================================================================
684 684 # TEST FUNCTIONS AND CREATORS
685 685 #==============================================================================
686 686 def create_test_index(repo_location, config):
687 687 """
688 688 Makes default test index.
689 689 """
690 690 import rc_testdata
691 691
692 692 rc_testdata.extract_search_index(
693 693 'vcs_search_index', os.path.dirname(config['search.location']))
694 694
695 695
696 696 def create_test_directory(test_path):
697 697 """
698 698 Create test directory if it doesn't exist.
699 699 """
700 700 if not os.path.isdir(test_path):
701 701 log.debug('Creating testdir %s', test_path)
702 702 os.makedirs(test_path)
703 703
704 704
705 705 def create_test_database(test_path, config):
706 706 """
707 707 Makes a fresh database.
708 708 """
709 709 from rhodecode.lib.db_manage import DbManage
710 710
711 711 # PART ONE create db
712 712 dbconf = config['sqlalchemy.db1.url']
713 713 log.debug('making test db %s', dbconf)
714 714
715 715 dbmanage = DbManage(log_sql=False, dbconf=dbconf, root=config['here'],
716 716 tests=True, cli_args={'force_ask': True})
717 717 dbmanage.create_tables(override=True)
718 718 dbmanage.set_db_version()
719 719 # for tests dynamically set new root paths based on generated content
720 720 dbmanage.create_settings(dbmanage.config_prompt(test_path))
721 721 dbmanage.create_default_user()
722 722 dbmanage.create_test_admin_and_users()
723 723 dbmanage.create_permissions()
724 724 dbmanage.populate_default_permissions()
725 725 Session().commit()
726 726
727 727
728 728 def create_test_repositories(test_path, config):
729 729 """
730 730 Creates test repositories in the temporary directory. Repositories are
731 731 extracted from archives within the rc_testdata package.
732 732 """
733 733 import rc_testdata
734 734 from rhodecode.tests import HG_REPO, GIT_REPO, SVN_REPO
735 735
736 736 log.debug('making test vcs repositories')
737 737
738 738 idx_path = config['search.location']
739 739 data_path = config['cache_dir']
740 740
741 741 # clean index and data
742 742 if idx_path and os.path.exists(idx_path):
743 743 log.debug('remove %s', idx_path)
744 744 shutil.rmtree(idx_path)
745 745
746 746 if data_path and os.path.exists(data_path):
747 747 log.debug('remove %s', data_path)
748 748 shutil.rmtree(data_path)
749 749
750 750 rc_testdata.extract_hg_dump('vcs_test_hg', jn(test_path, HG_REPO))
751 751 rc_testdata.extract_git_dump('vcs_test_git', jn(test_path, GIT_REPO))
752 752
753 753 # Note: Subversion is in the process of being integrated with the system,
754 754 # until we have a properly packed version of the test svn repository, this
755 755 # tries to copy over the repo from a package "rc_testdata"
756 756 svn_repo_path = rc_testdata.get_svn_repo_archive()
757 757 with tarfile.open(svn_repo_path) as tar:
758 758 tar.extractall(jn(test_path, SVN_REPO))
759 759
760 760
761 761 #==============================================================================
762 762 # PASTER COMMANDS
763 763 #==============================================================================
764 764 class BasePasterCommand(Command):
765 765 """
766 766 Abstract Base Class for paster commands.
767 767
768 768 The celery commands are somewhat aggressive about loading
769 769 celery.conf, and since our module sets the `CELERY_LOADER`
770 770 environment variable to our loader, we have to bootstrap a bit and
771 771 make sure we've had a chance to load the pylons config off of the
772 772 command line, otherwise everything fails.
773 773 """
774 774 min_args = 1
775 775 min_args_error = "Please provide a paster config file as an argument."
776 776 takes_config_file = 1
777 777 requires_config_file = True
778 778
779 779 def notify_msg(self, msg, log=False):
780 780 """Make a notification to user, additionally if logger is passed
781 781 it logs this action using given logger
782 782
783 783 :param msg: message that will be printed to user
784 784 :param log: logging instance, to use to additionally log this message
785 785
786 786 """
787 787 if log and isinstance(log, logging):
788 788 log(msg)
789 789
790 790 def run(self, args):
791 791 """
792 792 Overrides Command.run
793 793
794 794 Checks for a config file argument and loads it.
795 795 """
796 796 if len(args) < self.min_args:
797 797 raise BadCommand(
798 798 self.min_args_error % {'min_args': self.min_args,
799 799 'actual_args': len(args)})
800 800
801 801 # Decrement because we're going to lob off the first argument.
802 802 # @@ This is hacky
803 803 self.min_args -= 1
804 804 self.bootstrap_config(args[0])
805 805 self.update_parser()
806 806 return super(BasePasterCommand, self).run(args[1:])
807 807
808 808 def update_parser(self):
809 809 """
810 810 Abstract method. Allows for the class' parser to be updated
811 811 before the superclass' `run` method is called. Necessary to
812 812 allow options/arguments to be passed through to the underlying
813 813 celery command.
814 814 """
815 815 raise NotImplementedError("Abstract Method.")
816 816
817 817 def bootstrap_config(self, conf):
818 818 """
819 819 Loads the pylons configuration.
820 820 """
821 821 from pylons import config as pylonsconfig
822 822
823 823 self.path_to_ini_file = os.path.realpath(conf)
824 824 conf = paste.deploy.appconfig('config:' + self.path_to_ini_file)
825 825 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
826 826
827 827 def _init_session(self):
828 828 """
829 829 Inits SqlAlchemy Session
830 830 """
831 831 logging.config.fileConfig(self.path_to_ini_file)
832 832 from pylons import config
833 833 from rhodecode.config.utils import initialize_database
834 834
835 835 # get to remove repos !!
836 836 add_cache(config)
837 837 initialize_database(config)
838 838
839 839
840 840 @decorator.decorator
841 841 def jsonify(func, *args, **kwargs):
842 842 """Action decorator that formats output for JSON
843 843
844 844 Given a function that will return content, this decorator will turn
845 845 the result into JSON, with a content-type of 'application/json' and
846 846 output it.
847 847
848 848 """
849 849 from pylons.decorators.util import get_pylons
850 850 from rhodecode.lib.ext_json import json
851 851 pylons = get_pylons(args)
852 852 pylons.response.headers['Content-Type'] = 'application/json; charset=utf-8'
853 853 data = func(*args, **kwargs)
854 854 if isinstance(data, (list, tuple)):
855 855 msg = "JSON responses with Array envelopes are susceptible to " \
856 856 "cross-site data leak attacks, see " \
857 857 "http://wiki.pylonshq.com/display/pylonsfaq/Warnings"
858 858 warnings.warn(msg, Warning, 2)
859 859 log.warning(msg)
860 860 log.debug("Returning JSON wrapped action output")
861 861 return json.dumps(data, encoding='utf-8')
862 862
863 863
864 864 class PartialRenderer(object):
865 865 """
866 866 Partial renderer used to render chunks of html used in datagrids
867 867 use like::
868 868
869 869 _render = PartialRenderer('data_table/_dt_elements.mako')
870 870 _render('quick_menu', args, kwargs)
871 871 PartialRenderer.h,
872 872 c,
873 873 _,
874 874 ungettext
875 875 are the template stuff initialized inside and can be re-used later
876 876
877 877 :param tmpl_name: template path relate to /templates/ dir
878 878 """
879 879
880 880 def __init__(self, tmpl_name):
881 881 import rhodecode
882 882 from pylons import request, tmpl_context as c
883 883 from pylons.i18n.translation import _, ungettext
884 884 from rhodecode.lib import helpers as h
885 885
886 886 self.tmpl_name = tmpl_name
887 887 self.rhodecode = rhodecode
888 888 self.c = c
889 889 self._ = _
890 890 self.ungettext = ungettext
891 891 self.h = h
892 892 self.request = request
893 893
894 894 def _mako_lookup(self):
895 895 _tmpl_lookup = self.rhodecode.CONFIG['pylons.app_globals'].mako_lookup
896 896 return _tmpl_lookup.get_template(self.tmpl_name)
897 897
898 898 def _update_kwargs_for_render(self, kwargs):
899 899 """
900 900 Inject params required for Mako rendering
901 901 """
902 902 _kwargs = {
903 903 '_': self._,
904 904 'h': self.h,
905 905 'c': self.c,
906 906 'request': self.request,
907 907 '_ungettext': self.ungettext,
908 908 }
909 909 _kwargs.update(kwargs)
910 910 return _kwargs
911 911
912 912 def _render_with_exc(self, render_func, args, kwargs):
913 913 try:
914 914 return render_func.render(*args, **kwargs)
915 915 except:
916 916 log.error(exceptions.text_error_template().render())
917 917 raise
918 918
919 919 def _get_template(self, template_obj, def_name):
920 920 if def_name:
921 921 tmpl = template_obj.get_def(def_name)
922 922 else:
923 923 tmpl = template_obj
924 924 return tmpl
925 925
926 926 def render(self, def_name, *args, **kwargs):
927 927 lookup_obj = self._mako_lookup()
928 928 tmpl = self._get_template(lookup_obj, def_name=def_name)
929 929 kwargs = self._update_kwargs_for_render(kwargs)
930 930 return self._render_with_exc(tmpl, args, kwargs)
931 931
932 932 def __call__(self, tmpl, *args, **kwargs):
933 933 return self.render(tmpl, *args, **kwargs)
934 934
935 935
936 936 def password_changed(auth_user, session):
937 937 # Never report password change in case of default user or anonymous user.
938 938 if auth_user.username == User.DEFAULT_USER or auth_user.user_id is None:
939 939 return False
940 940
941 941 password_hash = md5(auth_user.password) if auth_user.password else None
942 942 rhodecode_user = session.get('rhodecode_user', {})
943 943 session_password_hash = rhodecode_user.get('password', '')
944 944 return password_hash != session_password_hash
945 945
946 946
947 947 def read_opensource_licenses():
948 948 global _license_cache
949 949
950 950 if not _license_cache:
951 951 licenses = pkg_resources.resource_string(
952 952 'rhodecode', 'config/licenses.json')
953 953 _license_cache = json.loads(licenses)
954 954
955 955 return _license_cache
956 956
957 957
958 958 def get_registry(request):
959 959 """
960 960 Utility to get the pyramid registry from a request. During migration to
961 961 pyramid we sometimes want to use the pyramid registry from pylons context.
962 962 Therefore this utility returns `request.registry` for pyramid requests and
963 963 uses `get_current_registry()` for pylons requests.
964 964 """
965 965 try:
966 966 return request.registry
967 967 except AttributeError:
968 968 return get_current_registry()
969 969
970 970
971 971 def generate_platform_uuid():
972 972 """
973 973 Generates platform UUID based on it's name
974 974 """
975 975 import platform
976 976
977 977 try:
978 978 uuid_list = [platform.platform()]
979 979 return hashlib.sha256(':'.join(uuid_list)).hexdigest()
980 980 except Exception as e:
981 981 log.error('Failed to generate host uuid: %s' % e)
982 982 return 'UNDEFINED'
@@ -1,4119 +1,4128 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Database Models for RhodeCode Enterprise
23 23 """
24 24
25 25 import re
26 26 import os
27 27 import time
28 28 import hashlib
29 29 import logging
30 30 import datetime
31 31 import warnings
32 32 import ipaddress
33 33 import functools
34 34 import traceback
35 35 import collections
36 36
37 37
38 38 from sqlalchemy import *
39 39 from sqlalchemy.ext.declarative import declared_attr
40 40 from sqlalchemy.ext.hybrid import hybrid_property
41 41 from sqlalchemy.orm import (
42 42 relationship, joinedload, class_mapper, validates, aliased)
43 43 from sqlalchemy.sql.expression import true
44 from sqlalchemy.sql.functions import coalesce, count # noqa
44 45 from beaker.cache import cache_region
45 46 from zope.cachedescriptors.property import Lazy as LazyProperty
46 47
47 48 from pyramid.threadlocal import get_current_request
48 49
49 50 from rhodecode.translation import _
50 51 from rhodecode.lib.vcs import get_vcs_instance
51 52 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
52 53 from rhodecode.lib.utils2 import (
53 54 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
54 55 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
55 56 glob2re, StrictAttributeDict, cleaned_uri)
56 57 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
57 58 from rhodecode.lib.ext_json import json
58 59 from rhodecode.lib.caching_query import FromCache
59 60 from rhodecode.lib.encrypt import AESCipher
60 61
61 62 from rhodecode.model.meta import Base, Session
62 63
63 64 URL_SEP = '/'
64 65 log = logging.getLogger(__name__)
65 66
66 67 # =============================================================================
67 68 # BASE CLASSES
68 69 # =============================================================================
69 70
70 71 # this is propagated from .ini file rhodecode.encrypted_values.secret or
71 72 # beaker.session.secret if first is not set.
72 73 # and initialized at environment.py
73 74 ENCRYPTION_KEY = None
74 75
75 76 # used to sort permissions by types, '#' used here is not allowed to be in
76 77 # usernames, and it's very early in sorted string.printable table.
77 78 PERMISSION_TYPE_SORT = {
78 79 'admin': '####',
79 80 'write': '###',
80 81 'read': '##',
81 82 'none': '#',
82 83 }
83 84
84 85
85 86 def display_sort(obj):
86 87 """
87 88 Sort function used to sort permissions in .permissions() function of
88 89 Repository, RepoGroup, UserGroup. Also it put the default user in front
89 90 of all other resources
90 91 """
91 92
92 93 if obj.username == User.DEFAULT_USER:
93 94 return '#####'
94 95 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
95 96 return prefix + obj.username
96 97
97 98
98 99 def _hash_key(k):
99 100 return md5_safe(k)
100 101
101 102
102 103 class EncryptedTextValue(TypeDecorator):
103 104 """
104 105 Special column for encrypted long text data, use like::
105 106
106 107 value = Column("encrypted_value", EncryptedValue(), nullable=False)
107 108
108 109 This column is intelligent so if value is in unencrypted form it return
109 110 unencrypted form, but on save it always encrypts
110 111 """
111 112 impl = Text
112 113
113 114 def process_bind_param(self, value, dialect):
114 115 if not value:
115 116 return value
116 117 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
117 118 # protect against double encrypting if someone manually starts
118 119 # doing
119 120 raise ValueError('value needs to be in unencrypted format, ie. '
120 121 'not starting with enc$aes')
121 122 return 'enc$aes_hmac$%s' % AESCipher(
122 123 ENCRYPTION_KEY, hmac=True).encrypt(value)
123 124
124 125 def process_result_value(self, value, dialect):
125 126 import rhodecode
126 127
127 128 if not value:
128 129 return value
129 130
130 131 parts = value.split('$', 3)
131 132 if not len(parts) == 3:
132 133 # probably not encrypted values
133 134 return value
134 135 else:
135 136 if parts[0] != 'enc':
136 137 # parts ok but without our header ?
137 138 return value
138 139 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
139 140 'rhodecode.encrypted_values.strict') or True)
140 141 # at that stage we know it's our encryption
141 142 if parts[1] == 'aes':
142 143 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
143 144 elif parts[1] == 'aes_hmac':
144 145 decrypted_data = AESCipher(
145 146 ENCRYPTION_KEY, hmac=True,
146 147 strict_verification=enc_strict_mode).decrypt(parts[2])
147 148 else:
148 149 raise ValueError(
149 150 'Encryption type part is wrong, must be `aes` '
150 151 'or `aes_hmac`, got `%s` instead' % (parts[1]))
151 152 return decrypted_data
152 153
153 154
154 155 class BaseModel(object):
155 156 """
156 157 Base Model for all classes
157 158 """
158 159
159 160 @classmethod
160 161 def _get_keys(cls):
161 162 """return column names for this model """
162 163 return class_mapper(cls).c.keys()
163 164
164 165 def get_dict(self):
165 166 """
166 167 return dict with keys and values corresponding
167 168 to this model data """
168 169
169 170 d = {}
170 171 for k in self._get_keys():
171 172 d[k] = getattr(self, k)
172 173
173 174 # also use __json__() if present to get additional fields
174 175 _json_attr = getattr(self, '__json__', None)
175 176 if _json_attr:
176 177 # update with attributes from __json__
177 178 if callable(_json_attr):
178 179 _json_attr = _json_attr()
179 180 for k, val in _json_attr.iteritems():
180 181 d[k] = val
181 182 return d
182 183
183 184 def get_appstruct(self):
184 185 """return list with keys and values tuples corresponding
185 186 to this model data """
186 187
187 188 l = []
188 189 for k in self._get_keys():
189 190 l.append((k, getattr(self, k),))
190 191 return l
191 192
192 193 def populate_obj(self, populate_dict):
193 194 """populate model with data from given populate_dict"""
194 195
195 196 for k in self._get_keys():
196 197 if k in populate_dict:
197 198 setattr(self, k, populate_dict[k])
198 199
199 200 @classmethod
200 201 def query(cls):
201 202 return Session().query(cls)
202 203
203 204 @classmethod
204 205 def get(cls, id_):
205 206 if id_:
206 207 return cls.query().get(id_)
207 208
208 209 @classmethod
209 210 def get_or_404(cls, id_):
210 211 from pyramid.httpexceptions import HTTPNotFound
211 212
212 213 try:
213 214 id_ = int(id_)
214 215 except (TypeError, ValueError):
215 216 raise HTTPNotFound()
216 217
217 218 res = cls.query().get(id_)
218 219 if not res:
219 220 raise HTTPNotFound()
220 221 return res
221 222
222 223 @classmethod
223 224 def getAll(cls):
224 225 # deprecated and left for backward compatibility
225 226 return cls.get_all()
226 227
227 228 @classmethod
228 229 def get_all(cls):
229 230 return cls.query().all()
230 231
231 232 @classmethod
232 233 def delete(cls, id_):
233 234 obj = cls.query().get(id_)
234 235 Session().delete(obj)
235 236
236 237 @classmethod
237 238 def identity_cache(cls, session, attr_name, value):
238 239 exist_in_session = []
239 240 for (item_cls, pkey), instance in session.identity_map.items():
240 241 if cls == item_cls and getattr(instance, attr_name) == value:
241 242 exist_in_session.append(instance)
242 243 if exist_in_session:
243 244 if len(exist_in_session) == 1:
244 245 return exist_in_session[0]
245 246 log.exception(
246 247 'multiple objects with attr %s and '
247 248 'value %s found with same name: %r',
248 249 attr_name, value, exist_in_session)
249 250
250 251 def __repr__(self):
251 252 if hasattr(self, '__unicode__'):
252 253 # python repr needs to return str
253 254 try:
254 255 return safe_str(self.__unicode__())
255 256 except UnicodeDecodeError:
256 257 pass
257 258 return '<DB:%s>' % (self.__class__.__name__)
258 259
259 260
260 261 class RhodeCodeSetting(Base, BaseModel):
261 262 __tablename__ = 'rhodecode_settings'
262 263 __table_args__ = (
263 264 UniqueConstraint('app_settings_name'),
264 265 {'extend_existing': True, 'mysql_engine': 'InnoDB',
265 266 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
266 267 )
267 268
268 269 SETTINGS_TYPES = {
269 270 'str': safe_str,
270 271 'int': safe_int,
271 272 'unicode': safe_unicode,
272 273 'bool': str2bool,
273 274 'list': functools.partial(aslist, sep=',')
274 275 }
275 276 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
276 277 GLOBAL_CONF_KEY = 'app_settings'
277 278
278 279 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
279 280 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
280 281 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
281 282 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
282 283
283 284 def __init__(self, key='', val='', type='unicode'):
284 285 self.app_settings_name = key
285 286 self.app_settings_type = type
286 287 self.app_settings_value = val
287 288
288 289 @validates('_app_settings_value')
289 290 def validate_settings_value(self, key, val):
290 291 assert type(val) == unicode
291 292 return val
292 293
293 294 @hybrid_property
294 295 def app_settings_value(self):
295 296 v = self._app_settings_value
296 297 _type = self.app_settings_type
297 298 if _type:
298 299 _type = self.app_settings_type.split('.')[0]
299 300 # decode the encrypted value
300 301 if 'encrypted' in self.app_settings_type:
301 302 cipher = EncryptedTextValue()
302 303 v = safe_unicode(cipher.process_result_value(v, None))
303 304
304 305 converter = self.SETTINGS_TYPES.get(_type) or \
305 306 self.SETTINGS_TYPES['unicode']
306 307 return converter(v)
307 308
308 309 @app_settings_value.setter
309 310 def app_settings_value(self, val):
310 311 """
311 312 Setter that will always make sure we use unicode in app_settings_value
312 313
313 314 :param val:
314 315 """
315 316 val = safe_unicode(val)
316 317 # encode the encrypted value
317 318 if 'encrypted' in self.app_settings_type:
318 319 cipher = EncryptedTextValue()
319 320 val = safe_unicode(cipher.process_bind_param(val, None))
320 321 self._app_settings_value = val
321 322
322 323 @hybrid_property
323 324 def app_settings_type(self):
324 325 return self._app_settings_type
325 326
326 327 @app_settings_type.setter
327 328 def app_settings_type(self, val):
328 329 if val.split('.')[0] not in self.SETTINGS_TYPES:
329 330 raise Exception('type must be one of %s got %s'
330 331 % (self.SETTINGS_TYPES.keys(), val))
331 332 self._app_settings_type = val
332 333
333 334 def __unicode__(self):
334 335 return u"<%s('%s:%s[%s]')>" % (
335 336 self.__class__.__name__,
336 337 self.app_settings_name, self.app_settings_value,
337 338 self.app_settings_type
338 339 )
339 340
340 341
341 342 class RhodeCodeUi(Base, BaseModel):
342 343 __tablename__ = 'rhodecode_ui'
343 344 __table_args__ = (
344 345 UniqueConstraint('ui_key'),
345 346 {'extend_existing': True, 'mysql_engine': 'InnoDB',
346 347 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
347 348 )
348 349
349 350 HOOK_REPO_SIZE = 'changegroup.repo_size'
350 351 # HG
351 352 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
352 353 HOOK_PULL = 'outgoing.pull_logger'
353 354 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
354 355 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
355 356 HOOK_PUSH = 'changegroup.push_logger'
356 357 HOOK_PUSH_KEY = 'pushkey.key_push'
357 358
358 359 # TODO: johbo: Unify way how hooks are configured for git and hg,
359 360 # git part is currently hardcoded.
360 361
361 362 # SVN PATTERNS
362 363 SVN_BRANCH_ID = 'vcs_svn_branch'
363 364 SVN_TAG_ID = 'vcs_svn_tag'
364 365
365 366 ui_id = Column(
366 367 "ui_id", Integer(), nullable=False, unique=True, default=None,
367 368 primary_key=True)
368 369 ui_section = Column(
369 370 "ui_section", String(255), nullable=True, unique=None, default=None)
370 371 ui_key = Column(
371 372 "ui_key", String(255), nullable=True, unique=None, default=None)
372 373 ui_value = Column(
373 374 "ui_value", String(255), nullable=True, unique=None, default=None)
374 375 ui_active = Column(
375 376 "ui_active", Boolean(), nullable=True, unique=None, default=True)
376 377
377 378 def __repr__(self):
378 379 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
379 380 self.ui_key, self.ui_value)
380 381
381 382
382 383 class RepoRhodeCodeSetting(Base, BaseModel):
383 384 __tablename__ = 'repo_rhodecode_settings'
384 385 __table_args__ = (
385 386 UniqueConstraint(
386 387 'app_settings_name', 'repository_id',
387 388 name='uq_repo_rhodecode_setting_name_repo_id'),
388 389 {'extend_existing': True, 'mysql_engine': 'InnoDB',
389 390 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
390 391 )
391 392
392 393 repository_id = Column(
393 394 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
394 395 nullable=False)
395 396 app_settings_id = Column(
396 397 "app_settings_id", Integer(), nullable=False, unique=True,
397 398 default=None, primary_key=True)
398 399 app_settings_name = Column(
399 400 "app_settings_name", String(255), nullable=True, unique=None,
400 401 default=None)
401 402 _app_settings_value = Column(
402 403 "app_settings_value", String(4096), nullable=True, unique=None,
403 404 default=None)
404 405 _app_settings_type = Column(
405 406 "app_settings_type", String(255), nullable=True, unique=None,
406 407 default=None)
407 408
408 409 repository = relationship('Repository')
409 410
410 411 def __init__(self, repository_id, key='', val='', type='unicode'):
411 412 self.repository_id = repository_id
412 413 self.app_settings_name = key
413 414 self.app_settings_type = type
414 415 self.app_settings_value = val
415 416
416 417 @validates('_app_settings_value')
417 418 def validate_settings_value(self, key, val):
418 419 assert type(val) == unicode
419 420 return val
420 421
421 422 @hybrid_property
422 423 def app_settings_value(self):
423 424 v = self._app_settings_value
424 425 type_ = self.app_settings_type
425 426 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
426 427 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
427 428 return converter(v)
428 429
429 430 @app_settings_value.setter
430 431 def app_settings_value(self, val):
431 432 """
432 433 Setter that will always make sure we use unicode in app_settings_value
433 434
434 435 :param val:
435 436 """
436 437 self._app_settings_value = safe_unicode(val)
437 438
438 439 @hybrid_property
439 440 def app_settings_type(self):
440 441 return self._app_settings_type
441 442
442 443 @app_settings_type.setter
443 444 def app_settings_type(self, val):
444 445 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
445 446 if val not in SETTINGS_TYPES:
446 447 raise Exception('type must be one of %s got %s'
447 448 % (SETTINGS_TYPES.keys(), val))
448 449 self._app_settings_type = val
449 450
450 451 def __unicode__(self):
451 452 return u"<%s('%s:%s:%s[%s]')>" % (
452 453 self.__class__.__name__, self.repository.repo_name,
453 454 self.app_settings_name, self.app_settings_value,
454 455 self.app_settings_type
455 456 )
456 457
457 458
458 459 class RepoRhodeCodeUi(Base, BaseModel):
459 460 __tablename__ = 'repo_rhodecode_ui'
460 461 __table_args__ = (
461 462 UniqueConstraint(
462 463 'repository_id', 'ui_section', 'ui_key',
463 464 name='uq_repo_rhodecode_ui_repository_id_section_key'),
464 465 {'extend_existing': True, 'mysql_engine': 'InnoDB',
465 466 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
466 467 )
467 468
468 469 repository_id = Column(
469 470 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
470 471 nullable=False)
471 472 ui_id = Column(
472 473 "ui_id", Integer(), nullable=False, unique=True, default=None,
473 474 primary_key=True)
474 475 ui_section = Column(
475 476 "ui_section", String(255), nullable=True, unique=None, default=None)
476 477 ui_key = Column(
477 478 "ui_key", String(255), nullable=True, unique=None, default=None)
478 479 ui_value = Column(
479 480 "ui_value", String(255), nullable=True, unique=None, default=None)
480 481 ui_active = Column(
481 482 "ui_active", Boolean(), nullable=True, unique=None, default=True)
482 483
483 484 repository = relationship('Repository')
484 485
485 486 def __repr__(self):
486 487 return '<%s[%s:%s]%s=>%s]>' % (
487 488 self.__class__.__name__, self.repository.repo_name,
488 489 self.ui_section, self.ui_key, self.ui_value)
489 490
490 491
491 492 class User(Base, BaseModel):
492 493 __tablename__ = 'users'
493 494 __table_args__ = (
494 495 UniqueConstraint('username'), UniqueConstraint('email'),
495 496 Index('u_username_idx', 'username'),
496 497 Index('u_email_idx', 'email'),
497 498 {'extend_existing': True, 'mysql_engine': 'InnoDB',
498 499 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
499 500 )
500 501 DEFAULT_USER = 'default'
501 502 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
502 503 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
503 504
504 505 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
505 506 username = Column("username", String(255), nullable=True, unique=None, default=None)
506 507 password = Column("password", String(255), nullable=True, unique=None, default=None)
507 508 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
508 509 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
509 510 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
510 511 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
511 512 _email = Column("email", String(255), nullable=True, unique=None, default=None)
512 513 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
513 514 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
514 515
515 516 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
516 517 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
517 518 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
518 519 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
519 520 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
520 521 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
521 522
522 523 user_log = relationship('UserLog')
523 524 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
524 525
525 526 repositories = relationship('Repository')
526 527 repository_groups = relationship('RepoGroup')
527 528 user_groups = relationship('UserGroup')
528 529
529 530 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
530 531 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
531 532
532 533 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
533 534 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
534 535 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
535 536
536 537 group_member = relationship('UserGroupMember', cascade='all')
537 538
538 539 notifications = relationship('UserNotification', cascade='all')
539 540 # notifications assigned to this user
540 541 user_created_notifications = relationship('Notification', cascade='all')
541 542 # comments created by this user
542 543 user_comments = relationship('ChangesetComment', cascade='all')
543 544 # user profile extra info
544 545 user_emails = relationship('UserEmailMap', cascade='all')
545 546 user_ip_map = relationship('UserIpMap', cascade='all')
546 547 user_auth_tokens = relationship('UserApiKeys', cascade='all')
547 548 # gists
548 549 user_gists = relationship('Gist', cascade='all')
549 550 # user pull requests
550 551 user_pull_requests = relationship('PullRequest', cascade='all')
551 552 # external identities
552 553 extenal_identities = relationship(
553 554 'ExternalIdentity',
554 555 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
555 556 cascade='all')
556 557
557 558 def __unicode__(self):
558 559 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
559 560 self.user_id, self.username)
560 561
561 562 @hybrid_property
562 563 def email(self):
563 564 return self._email
564 565
565 566 @email.setter
566 567 def email(self, val):
567 568 self._email = val.lower() if val else None
568 569
569 570 @hybrid_property
570 571 def first_name(self):
571 572 from rhodecode.lib import helpers as h
572 573 if self.name:
573 574 return h.escape(self.name)
574 575 return self.name
575 576
576 577 @hybrid_property
577 578 def last_name(self):
578 579 from rhodecode.lib import helpers as h
579 580 if self.lastname:
580 581 return h.escape(self.lastname)
581 582 return self.lastname
582 583
583 584 @hybrid_property
584 585 def api_key(self):
585 586 """
586 587 Fetch if exist an auth-token with role ALL connected to this user
587 588 """
588 589 user_auth_token = UserApiKeys.query()\
589 590 .filter(UserApiKeys.user_id == self.user_id)\
590 591 .filter(or_(UserApiKeys.expires == -1,
591 592 UserApiKeys.expires >= time.time()))\
592 593 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
593 594 if user_auth_token:
594 595 user_auth_token = user_auth_token.api_key
595 596
596 597 return user_auth_token
597 598
598 599 @api_key.setter
599 600 def api_key(self, val):
600 601 # don't allow to set API key this is deprecated for now
601 602 self._api_key = None
602 603
603 604 @property
604 605 def reviewer_pull_requests(self):
605 606 return PullRequestReviewers.query() \
606 607 .options(joinedload(PullRequestReviewers.pull_request)) \
607 608 .filter(PullRequestReviewers.user_id == self.user_id) \
608 609 .all()
609 610
610 611 @property
611 612 def firstname(self):
612 613 # alias for future
613 614 return self.name
614 615
615 616 @property
616 617 def emails(self):
617 618 other = UserEmailMap.query().filter(UserEmailMap.user == self).all()
618 619 return [self.email] + [x.email for x in other]
619 620
620 621 @property
621 622 def auth_tokens(self):
622 623 auth_tokens = self.get_auth_tokens()
623 624 return [x.api_key for x in auth_tokens]
624 625
625 626 def get_auth_tokens(self):
626 627 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
627 628
628 629 @property
629 630 def feed_token(self):
630 631 return self.get_feed_token()
631 632
632 633 def get_feed_token(self):
633 634 feed_tokens = UserApiKeys.query()\
634 635 .filter(UserApiKeys.user == self)\
635 636 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
636 637 .all()
637 638 if feed_tokens:
638 639 return feed_tokens[0].api_key
639 640 return 'NO_FEED_TOKEN_AVAILABLE'
640 641
641 642 @classmethod
642 643 def extra_valid_auth_tokens(cls, user, role=None):
643 644 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
644 645 .filter(or_(UserApiKeys.expires == -1,
645 646 UserApiKeys.expires >= time.time()))
646 647 if role:
647 648 tokens = tokens.filter(or_(UserApiKeys.role == role,
648 649 UserApiKeys.role == UserApiKeys.ROLE_ALL))
649 650 return tokens.all()
650 651
651 652 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
652 653 from rhodecode.lib import auth
653 654
654 655 log.debug('Trying to authenticate user: %s via auth-token, '
655 656 'and roles: %s', self, roles)
656 657
657 658 if not auth_token:
658 659 return False
659 660
660 661 crypto_backend = auth.crypto_backend()
661 662
662 663 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
663 664 tokens_q = UserApiKeys.query()\
664 665 .filter(UserApiKeys.user_id == self.user_id)\
665 666 .filter(or_(UserApiKeys.expires == -1,
666 667 UserApiKeys.expires >= time.time()))
667 668
668 669 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
669 670
670 671 plain_tokens = []
671 672 hash_tokens = []
672 673
673 674 for token in tokens_q.all():
674 675 # verify scope first
675 676 if token.repo_id:
676 677 # token has a scope, we need to verify it
677 678 if scope_repo_id != token.repo_id:
678 679 log.debug(
679 680 'Scope mismatch: token has a set repo scope: %s, '
680 681 'and calling scope is:%s, skipping further checks',
681 682 token.repo, scope_repo_id)
682 683 # token has a scope, and it doesn't match, skip token
683 684 continue
684 685
685 686 if token.api_key.startswith(crypto_backend.ENC_PREF):
686 687 hash_tokens.append(token.api_key)
687 688 else:
688 689 plain_tokens.append(token.api_key)
689 690
690 691 is_plain_match = auth_token in plain_tokens
691 692 if is_plain_match:
692 693 return True
693 694
694 695 for hashed in hash_tokens:
695 696 # TODO(marcink): this is expensive to calculate, but most secure
696 697 match = crypto_backend.hash_check(auth_token, hashed)
697 698 if match:
698 699 return True
699 700
700 701 return False
701 702
702 703 @property
703 704 def ip_addresses(self):
704 705 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
705 706 return [x.ip_addr for x in ret]
706 707
707 708 @property
708 709 def username_and_name(self):
709 710 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
710 711
711 712 @property
712 713 def username_or_name_or_email(self):
713 714 full_name = self.full_name if self.full_name is not ' ' else None
714 715 return self.username or full_name or self.email
715 716
716 717 @property
717 718 def full_name(self):
718 719 return '%s %s' % (self.first_name, self.last_name)
719 720
720 721 @property
721 722 def full_name_or_username(self):
722 723 return ('%s %s' % (self.first_name, self.last_name)
723 724 if (self.first_name and self.last_name) else self.username)
724 725
725 726 @property
726 727 def full_contact(self):
727 728 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
728 729
729 730 @property
730 731 def short_contact(self):
731 732 return '%s %s' % (self.first_name, self.last_name)
732 733
733 734 @property
734 735 def is_admin(self):
735 736 return self.admin
736 737
737 738 @property
738 739 def AuthUser(self):
739 740 """
740 741 Returns instance of AuthUser for this user
741 742 """
742 743 from rhodecode.lib.auth import AuthUser
743 744 return AuthUser(user_id=self.user_id, username=self.username)
744 745
745 746 @hybrid_property
746 747 def user_data(self):
747 748 if not self._user_data:
748 749 return {}
749 750
750 751 try:
751 752 return json.loads(self._user_data)
752 753 except TypeError:
753 754 return {}
754 755
755 756 @user_data.setter
756 757 def user_data(self, val):
757 758 if not isinstance(val, dict):
758 759 raise Exception('user_data must be dict, got %s' % type(val))
759 760 try:
760 761 self._user_data = json.dumps(val)
761 762 except Exception:
762 763 log.error(traceback.format_exc())
763 764
764 765 @classmethod
765 766 def get_by_username(cls, username, case_insensitive=False,
766 767 cache=False, identity_cache=False):
767 768 session = Session()
768 769
769 770 if case_insensitive:
770 771 q = cls.query().filter(
771 772 func.lower(cls.username) == func.lower(username))
772 773 else:
773 774 q = cls.query().filter(cls.username == username)
774 775
775 776 if cache:
776 777 if identity_cache:
777 778 val = cls.identity_cache(session, 'username', username)
778 779 if val:
779 780 return val
780 781 else:
781 782 cache_key = "get_user_by_name_%s" % _hash_key(username)
782 783 q = q.options(
783 784 FromCache("sql_cache_short", cache_key))
784 785
785 786 return q.scalar()
786 787
787 788 @classmethod
788 789 def get_by_auth_token(cls, auth_token, cache=False):
789 790 q = UserApiKeys.query()\
790 791 .filter(UserApiKeys.api_key == auth_token)\
791 792 .filter(or_(UserApiKeys.expires == -1,
792 793 UserApiKeys.expires >= time.time()))
793 794 if cache:
794 795 q = q.options(
795 796 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
796 797
797 798 match = q.first()
798 799 if match:
799 800 return match.user
800 801
801 802 @classmethod
802 803 def get_by_email(cls, email, case_insensitive=False, cache=False):
803 804
804 805 if case_insensitive:
805 806 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
806 807
807 808 else:
808 809 q = cls.query().filter(cls.email == email)
809 810
810 811 email_key = _hash_key(email)
811 812 if cache:
812 813 q = q.options(
813 814 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
814 815
815 816 ret = q.scalar()
816 817 if ret is None:
817 818 q = UserEmailMap.query()
818 819 # try fetching in alternate email map
819 820 if case_insensitive:
820 821 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
821 822 else:
822 823 q = q.filter(UserEmailMap.email == email)
823 824 q = q.options(joinedload(UserEmailMap.user))
824 825 if cache:
825 826 q = q.options(
826 827 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
827 828 ret = getattr(q.scalar(), 'user', None)
828 829
829 830 return ret
830 831
831 832 @classmethod
832 833 def get_from_cs_author(cls, author):
833 834 """
834 835 Tries to get User objects out of commit author string
835 836
836 837 :param author:
837 838 """
838 839 from rhodecode.lib.helpers import email, author_name
839 840 # Valid email in the attribute passed, see if they're in the system
840 841 _email = email(author)
841 842 if _email:
842 843 user = cls.get_by_email(_email, case_insensitive=True)
843 844 if user:
844 845 return user
845 846 # Maybe we can match by username?
846 847 _author = author_name(author)
847 848 user = cls.get_by_username(_author, case_insensitive=True)
848 849 if user:
849 850 return user
850 851
851 852 def update_userdata(self, **kwargs):
852 853 usr = self
853 854 old = usr.user_data
854 855 old.update(**kwargs)
855 856 usr.user_data = old
856 857 Session().add(usr)
857 858 log.debug('updated userdata with ', kwargs)
858 859
859 860 def update_lastlogin(self):
860 861 """Update user lastlogin"""
861 862 self.last_login = datetime.datetime.now()
862 863 Session().add(self)
863 864 log.debug('updated user %s lastlogin', self.username)
864 865
865 866 def update_lastactivity(self):
866 867 """Update user lastactivity"""
867 868 self.last_activity = datetime.datetime.now()
868 869 Session().add(self)
869 870 log.debug('updated user %s lastactivity', self.username)
870 871
871 872 def update_password(self, new_password):
872 873 from rhodecode.lib.auth import get_crypt_password
873 874
874 875 self.password = get_crypt_password(new_password)
875 876 Session().add(self)
876 877
877 878 @classmethod
878 879 def get_first_super_admin(cls):
879 880 user = User.query().filter(User.admin == true()).first()
880 881 if user is None:
881 882 raise Exception('FATAL: Missing administrative account!')
882 883 return user
883 884
884 885 @classmethod
885 886 def get_all_super_admins(cls):
886 887 """
887 888 Returns all admin accounts sorted by username
888 889 """
889 890 return User.query().filter(User.admin == true())\
890 891 .order_by(User.username.asc()).all()
891 892
892 893 @classmethod
893 894 def get_default_user(cls, cache=False, refresh=False):
894 895 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
895 896 if user is None:
896 897 raise Exception('FATAL: Missing default account!')
897 898 if refresh:
898 899 # The default user might be based on outdated state which
899 900 # has been loaded from the cache.
900 901 # A call to refresh() ensures that the
901 902 # latest state from the database is used.
902 903 Session().refresh(user)
903 904 return user
904 905
905 906 def _get_default_perms(self, user, suffix=''):
906 907 from rhodecode.model.permission import PermissionModel
907 908 return PermissionModel().get_default_perms(user.user_perms, suffix)
908 909
909 910 def get_default_perms(self, suffix=''):
910 911 return self._get_default_perms(self, suffix)
911 912
912 913 def get_api_data(self, include_secrets=False, details='full'):
913 914 """
914 915 Common function for generating user related data for API
915 916
916 917 :param include_secrets: By default secrets in the API data will be replaced
917 918 by a placeholder value to prevent exposing this data by accident. In case
918 919 this data shall be exposed, set this flag to ``True``.
919 920
920 921 :param details: details can be 'basic|full' basic gives only a subset of
921 922 the available user information that includes user_id, name and emails.
922 923 """
923 924 user = self
924 925 user_data = self.user_data
925 926 data = {
926 927 'user_id': user.user_id,
927 928 'username': user.username,
928 929 'firstname': user.name,
929 930 'lastname': user.lastname,
930 931 'email': user.email,
931 932 'emails': user.emails,
932 933 }
933 934 if details == 'basic':
934 935 return data
935 936
936 937 auth_token_length = 40
937 938 auth_token_replacement = '*' * auth_token_length
938 939
939 940 extras = {
940 941 'auth_tokens': [auth_token_replacement],
941 942 'active': user.active,
942 943 'admin': user.admin,
943 944 'extern_type': user.extern_type,
944 945 'extern_name': user.extern_name,
945 946 'last_login': user.last_login,
946 947 'last_activity': user.last_activity,
947 948 'ip_addresses': user.ip_addresses,
948 949 'language': user_data.get('language')
949 950 }
950 951 data.update(extras)
951 952
952 953 if include_secrets:
953 954 data['auth_tokens'] = user.auth_tokens
954 955 return data
955 956
956 957 def __json__(self):
957 958 data = {
958 959 'full_name': self.full_name,
959 960 'full_name_or_username': self.full_name_or_username,
960 961 'short_contact': self.short_contact,
961 962 'full_contact': self.full_contact,
962 963 }
963 964 data.update(self.get_api_data())
964 965 return data
965 966
966 967
967 968 class UserApiKeys(Base, BaseModel):
968 969 __tablename__ = 'user_api_keys'
969 970 __table_args__ = (
970 971 Index('uak_api_key_idx', 'api_key', unique=True),
971 972 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
972 973 {'extend_existing': True, 'mysql_engine': 'InnoDB',
973 974 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
974 975 )
975 976 __mapper_args__ = {}
976 977
977 978 # ApiKey role
978 979 ROLE_ALL = 'token_role_all'
979 980 ROLE_HTTP = 'token_role_http'
980 981 ROLE_VCS = 'token_role_vcs'
981 982 ROLE_API = 'token_role_api'
982 983 ROLE_FEED = 'token_role_feed'
983 984 ROLE_PASSWORD_RESET = 'token_password_reset'
984 985
985 986 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
986 987
987 988 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
988 989 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
989 990 api_key = Column("api_key", String(255), nullable=False, unique=True)
990 991 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
991 992 expires = Column('expires', Float(53), nullable=False)
992 993 role = Column('role', String(255), nullable=True)
993 994 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
994 995
995 996 # scope columns
996 997 repo_id = Column(
997 998 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
998 999 nullable=True, unique=None, default=None)
999 1000 repo = relationship('Repository', lazy='joined')
1000 1001
1001 1002 repo_group_id = Column(
1002 1003 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1003 1004 nullable=True, unique=None, default=None)
1004 1005 repo_group = relationship('RepoGroup', lazy='joined')
1005 1006
1006 1007 user = relationship('User', lazy='joined')
1007 1008
1008 1009 def __unicode__(self):
1009 1010 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1010 1011
1011 1012 def __json__(self):
1012 1013 data = {
1013 1014 'auth_token': self.api_key,
1014 1015 'role': self.role,
1015 1016 'scope': self.scope_humanized,
1016 1017 'expired': self.expired
1017 1018 }
1018 1019 return data
1019 1020
1020 1021 def get_api_data(self, include_secrets=False):
1021 1022 data = self.__json__()
1022 1023 if include_secrets:
1023 1024 return data
1024 1025 else:
1025 1026 data['auth_token'] = self.token_obfuscated
1026 1027 return data
1027 1028
1028 1029 @hybrid_property
1029 1030 def description_safe(self):
1030 1031 from rhodecode.lib import helpers as h
1031 1032 return h.escape(self.description)
1032 1033
1033 1034 @property
1034 1035 def expired(self):
1035 1036 if self.expires == -1:
1036 1037 return False
1037 1038 return time.time() > self.expires
1038 1039
1039 1040 @classmethod
1040 1041 def _get_role_name(cls, role):
1041 1042 return {
1042 1043 cls.ROLE_ALL: _('all'),
1043 1044 cls.ROLE_HTTP: _('http/web interface'),
1044 1045 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1045 1046 cls.ROLE_API: _('api calls'),
1046 1047 cls.ROLE_FEED: _('feed access'),
1047 1048 }.get(role, role)
1048 1049
1049 1050 @property
1050 1051 def role_humanized(self):
1051 1052 return self._get_role_name(self.role)
1052 1053
1053 1054 def _get_scope(self):
1054 1055 if self.repo:
1055 1056 return repr(self.repo)
1056 1057 if self.repo_group:
1057 1058 return repr(self.repo_group) + ' (recursive)'
1058 1059 return 'global'
1059 1060
1060 1061 @property
1061 1062 def scope_humanized(self):
1062 1063 return self._get_scope()
1063 1064
1064 1065 @property
1065 1066 def token_obfuscated(self):
1066 1067 if self.api_key:
1067 1068 return self.api_key[:4] + "****"
1068 1069
1069 1070
1070 1071 class UserEmailMap(Base, BaseModel):
1071 1072 __tablename__ = 'user_email_map'
1072 1073 __table_args__ = (
1073 1074 Index('uem_email_idx', 'email'),
1074 1075 UniqueConstraint('email'),
1075 1076 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1076 1077 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1077 1078 )
1078 1079 __mapper_args__ = {}
1079 1080
1080 1081 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1081 1082 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1082 1083 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1083 1084 user = relationship('User', lazy='joined')
1084 1085
1085 1086 @validates('_email')
1086 1087 def validate_email(self, key, email):
1087 1088 # check if this email is not main one
1088 1089 main_email = Session().query(User).filter(User.email == email).scalar()
1089 1090 if main_email is not None:
1090 1091 raise AttributeError('email %s is present is user table' % email)
1091 1092 return email
1092 1093
1093 1094 @hybrid_property
1094 1095 def email(self):
1095 1096 return self._email
1096 1097
1097 1098 @email.setter
1098 1099 def email(self, val):
1099 1100 self._email = val.lower() if val else None
1100 1101
1101 1102
1102 1103 class UserIpMap(Base, BaseModel):
1103 1104 __tablename__ = 'user_ip_map'
1104 1105 __table_args__ = (
1105 1106 UniqueConstraint('user_id', 'ip_addr'),
1106 1107 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1107 1108 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1108 1109 )
1109 1110 __mapper_args__ = {}
1110 1111
1111 1112 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1112 1113 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1113 1114 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1114 1115 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1115 1116 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1116 1117 user = relationship('User', lazy='joined')
1117 1118
1118 1119 @hybrid_property
1119 1120 def description_safe(self):
1120 1121 from rhodecode.lib import helpers as h
1121 1122 return h.escape(self.description)
1122 1123
1123 1124 @classmethod
1124 1125 def _get_ip_range(cls, ip_addr):
1125 1126 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1126 1127 return [str(net.network_address), str(net.broadcast_address)]
1127 1128
1128 1129 def __json__(self):
1129 1130 return {
1130 1131 'ip_addr': self.ip_addr,
1131 1132 'ip_range': self._get_ip_range(self.ip_addr),
1132 1133 }
1133 1134
1134 1135 def __unicode__(self):
1135 1136 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1136 1137 self.user_id, self.ip_addr)
1137 1138
1138 1139
1139 1140 class UserLog(Base, BaseModel):
1140 1141 __tablename__ = 'user_logs'
1141 1142 __table_args__ = (
1142 1143 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1143 1144 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1144 1145 )
1145 1146 VERSION_1 = 'v1'
1146 1147 VERSION_2 = 'v2'
1147 1148 VERSIONS = [VERSION_1, VERSION_2]
1148 1149
1149 1150 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1150 1151 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1151 1152 username = Column("username", String(255), nullable=True, unique=None, default=None)
1152 1153 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1153 1154 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1154 1155 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1155 1156 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1156 1157 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1157 1158
1158 1159 version = Column("version", String(255), nullable=True, default=VERSION_1)
1159 1160 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1160 1161 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1161 1162
1162 1163 def __unicode__(self):
1163 1164 return u"<%s('id:%s:%s')>" % (
1164 1165 self.__class__.__name__, self.repository_name, self.action)
1165 1166
1166 1167 def __json__(self):
1167 1168 return {
1168 1169 'user_id': self.user_id,
1169 1170 'username': self.username,
1170 1171 'repository_id': self.repository_id,
1171 1172 'repository_name': self.repository_name,
1172 1173 'user_ip': self.user_ip,
1173 1174 'action_date': self.action_date,
1174 1175 'action': self.action,
1175 1176 }
1176 1177
1177 1178 @property
1178 1179 def action_as_day(self):
1179 1180 return datetime.date(*self.action_date.timetuple()[:3])
1180 1181
1181 1182 user = relationship('User')
1182 1183 repository = relationship('Repository', cascade='')
1183 1184
1184 1185
1185 1186 class UserGroup(Base, BaseModel):
1186 1187 __tablename__ = 'users_groups'
1187 1188 __table_args__ = (
1188 1189 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1189 1190 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1190 1191 )
1191 1192
1192 1193 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1193 1194 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1194 1195 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1195 1196 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1196 1197 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1197 1198 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1198 1199 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1199 1200 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1200 1201
1201 1202 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1202 1203 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1203 1204 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1204 1205 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1205 1206 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1206 1207 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1207 1208
1208 user = relationship('User')
1209 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1210
1211 @classmethod
1212 def _load_group_data(cls, column):
1213 if not column:
1214 return {}
1215
1216 try:
1217 return json.loads(column) or {}
1218 except TypeError:
1219 return {}
1209 1220
1210 1221 @hybrid_property
1211 1222 def description_safe(self):
1212 1223 from rhodecode.lib import helpers as h
1213 1224 return h.escape(self.description)
1214 1225
1215 1226 @hybrid_property
1216 1227 def group_data(self):
1217 if not self._group_data:
1218 return {}
1219
1220 try:
1221 return json.loads(self._group_data)
1222 except TypeError:
1223 return {}
1228 return self._load_group_data(self._group_data)
1229
1230 @group_data.expression
1231 def group_data(self, **kwargs):
1232 return self._group_data
1224 1233
1225 1234 @group_data.setter
1226 1235 def group_data(self, val):
1227 1236 try:
1228 1237 self._group_data = json.dumps(val)
1229 1238 except Exception:
1230 1239 log.error(traceback.format_exc())
1231 1240
1232 1241 def __unicode__(self):
1233 1242 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1234 1243 self.users_group_id,
1235 1244 self.users_group_name)
1236 1245
1237 1246 @classmethod
1238 1247 def get_by_group_name(cls, group_name, cache=False,
1239 1248 case_insensitive=False):
1240 1249 if case_insensitive:
1241 1250 q = cls.query().filter(func.lower(cls.users_group_name) ==
1242 1251 func.lower(group_name))
1243 1252
1244 1253 else:
1245 1254 q = cls.query().filter(cls.users_group_name == group_name)
1246 1255 if cache:
1247 1256 q = q.options(
1248 1257 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1249 1258 return q.scalar()
1250 1259
1251 1260 @classmethod
1252 1261 def get(cls, user_group_id, cache=False):
1253 1262 user_group = cls.query()
1254 1263 if cache:
1255 1264 user_group = user_group.options(
1256 1265 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1257 1266 return user_group.get(user_group_id)
1258 1267
1259 1268 def permissions(self, with_admins=True, with_owner=True):
1260 1269 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1261 1270 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1262 1271 joinedload(UserUserGroupToPerm.user),
1263 1272 joinedload(UserUserGroupToPerm.permission),)
1264 1273
1265 1274 # get owners and admins and permissions. We do a trick of re-writing
1266 1275 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1267 1276 # has a global reference and changing one object propagates to all
1268 1277 # others. This means if admin is also an owner admin_row that change
1269 1278 # would propagate to both objects
1270 1279 perm_rows = []
1271 1280 for _usr in q.all():
1272 1281 usr = AttributeDict(_usr.user.get_dict())
1273 1282 usr.permission = _usr.permission.permission_name
1274 1283 perm_rows.append(usr)
1275 1284
1276 1285 # filter the perm rows by 'default' first and then sort them by
1277 1286 # admin,write,read,none permissions sorted again alphabetically in
1278 1287 # each group
1279 1288 perm_rows = sorted(perm_rows, key=display_sort)
1280 1289
1281 1290 _admin_perm = 'usergroup.admin'
1282 1291 owner_row = []
1283 1292 if with_owner:
1284 1293 usr = AttributeDict(self.user.get_dict())
1285 1294 usr.owner_row = True
1286 1295 usr.permission = _admin_perm
1287 1296 owner_row.append(usr)
1288 1297
1289 1298 super_admin_rows = []
1290 1299 if with_admins:
1291 1300 for usr in User.get_all_super_admins():
1292 1301 # if this admin is also owner, don't double the record
1293 1302 if usr.user_id == owner_row[0].user_id:
1294 1303 owner_row[0].admin_row = True
1295 1304 else:
1296 1305 usr = AttributeDict(usr.get_dict())
1297 1306 usr.admin_row = True
1298 1307 usr.permission = _admin_perm
1299 1308 super_admin_rows.append(usr)
1300 1309
1301 1310 return super_admin_rows + owner_row + perm_rows
1302 1311
1303 1312 def permission_user_groups(self):
1304 1313 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1305 1314 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1306 1315 joinedload(UserGroupUserGroupToPerm.target_user_group),
1307 1316 joinedload(UserGroupUserGroupToPerm.permission),)
1308 1317
1309 1318 perm_rows = []
1310 1319 for _user_group in q.all():
1311 1320 usr = AttributeDict(_user_group.user_group.get_dict())
1312 1321 usr.permission = _user_group.permission.permission_name
1313 1322 perm_rows.append(usr)
1314 1323
1315 1324 return perm_rows
1316 1325
1317 1326 def _get_default_perms(self, user_group, suffix=''):
1318 1327 from rhodecode.model.permission import PermissionModel
1319 1328 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1320 1329
1321 1330 def get_default_perms(self, suffix=''):
1322 1331 return self._get_default_perms(self, suffix)
1323 1332
1324 1333 def get_api_data(self, with_group_members=True, include_secrets=False):
1325 1334 """
1326 1335 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1327 1336 basically forwarded.
1328 1337
1329 1338 """
1330 1339 user_group = self
1331 1340 data = {
1332 1341 'users_group_id': user_group.users_group_id,
1333 1342 'group_name': user_group.users_group_name,
1334 1343 'group_description': user_group.user_group_description,
1335 1344 'active': user_group.users_group_active,
1336 1345 'owner': user_group.user.username,
1337 1346 'owner_email': user_group.user.email,
1338 1347 }
1339 1348
1340 1349 if with_group_members:
1341 1350 users = []
1342 1351 for user in user_group.members:
1343 1352 user = user.user
1344 1353 users.append(user.get_api_data(include_secrets=include_secrets))
1345 1354 data['users'] = users
1346 1355
1347 1356 return data
1348 1357
1349 1358
1350 1359 class UserGroupMember(Base, BaseModel):
1351 1360 __tablename__ = 'users_groups_members'
1352 1361 __table_args__ = (
1353 1362 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1354 1363 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1355 1364 )
1356 1365
1357 1366 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1358 1367 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1359 1368 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1360 1369
1361 1370 user = relationship('User', lazy='joined')
1362 1371 users_group = relationship('UserGroup')
1363 1372
1364 1373 def __init__(self, gr_id='', u_id=''):
1365 1374 self.users_group_id = gr_id
1366 1375 self.user_id = u_id
1367 1376
1368 1377
1369 1378 class RepositoryField(Base, BaseModel):
1370 1379 __tablename__ = 'repositories_fields'
1371 1380 __table_args__ = (
1372 1381 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1373 1382 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1374 1383 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1375 1384 )
1376 1385 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1377 1386
1378 1387 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1379 1388 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1380 1389 field_key = Column("field_key", String(250))
1381 1390 field_label = Column("field_label", String(1024), nullable=False)
1382 1391 field_value = Column("field_value", String(10000), nullable=False)
1383 1392 field_desc = Column("field_desc", String(1024), nullable=False)
1384 1393 field_type = Column("field_type", String(255), nullable=False, unique=None)
1385 1394 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1386 1395
1387 1396 repository = relationship('Repository')
1388 1397
1389 1398 @property
1390 1399 def field_key_prefixed(self):
1391 1400 return 'ex_%s' % self.field_key
1392 1401
1393 1402 @classmethod
1394 1403 def un_prefix_key(cls, key):
1395 1404 if key.startswith(cls.PREFIX):
1396 1405 return key[len(cls.PREFIX):]
1397 1406 return key
1398 1407
1399 1408 @classmethod
1400 1409 def get_by_key_name(cls, key, repo):
1401 1410 row = cls.query()\
1402 1411 .filter(cls.repository == repo)\
1403 1412 .filter(cls.field_key == key).scalar()
1404 1413 return row
1405 1414
1406 1415
1407 1416 class Repository(Base, BaseModel):
1408 1417 __tablename__ = 'repositories'
1409 1418 __table_args__ = (
1410 1419 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1411 1420 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1412 1421 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1413 1422 )
1414 1423 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1415 1424 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1416 1425
1417 1426 STATE_CREATED = 'repo_state_created'
1418 1427 STATE_PENDING = 'repo_state_pending'
1419 1428 STATE_ERROR = 'repo_state_error'
1420 1429
1421 1430 LOCK_AUTOMATIC = 'lock_auto'
1422 1431 LOCK_API = 'lock_api'
1423 1432 LOCK_WEB = 'lock_web'
1424 1433 LOCK_PULL = 'lock_pull'
1425 1434
1426 1435 NAME_SEP = URL_SEP
1427 1436
1428 1437 repo_id = Column(
1429 1438 "repo_id", Integer(), nullable=False, unique=True, default=None,
1430 1439 primary_key=True)
1431 1440 _repo_name = Column(
1432 1441 "repo_name", Text(), nullable=False, default=None)
1433 1442 _repo_name_hash = Column(
1434 1443 "repo_name_hash", String(255), nullable=False, unique=True)
1435 1444 repo_state = Column("repo_state", String(255), nullable=True)
1436 1445
1437 1446 clone_uri = Column(
1438 1447 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1439 1448 default=None)
1440 1449 repo_type = Column(
1441 1450 "repo_type", String(255), nullable=False, unique=False, default=None)
1442 1451 user_id = Column(
1443 1452 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1444 1453 unique=False, default=None)
1445 1454 private = Column(
1446 1455 "private", Boolean(), nullable=True, unique=None, default=None)
1447 1456 enable_statistics = Column(
1448 1457 "statistics", Boolean(), nullable=True, unique=None, default=True)
1449 1458 enable_downloads = Column(
1450 1459 "downloads", Boolean(), nullable=True, unique=None, default=True)
1451 1460 description = Column(
1452 1461 "description", String(10000), nullable=True, unique=None, default=None)
1453 1462 created_on = Column(
1454 1463 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1455 1464 default=datetime.datetime.now)
1456 1465 updated_on = Column(
1457 1466 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1458 1467 default=datetime.datetime.now)
1459 1468 _landing_revision = Column(
1460 1469 "landing_revision", String(255), nullable=False, unique=False,
1461 1470 default=None)
1462 1471 enable_locking = Column(
1463 1472 "enable_locking", Boolean(), nullable=False, unique=None,
1464 1473 default=False)
1465 1474 _locked = Column(
1466 1475 "locked", String(255), nullable=True, unique=False, default=None)
1467 1476 _changeset_cache = Column(
1468 1477 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1469 1478
1470 1479 fork_id = Column(
1471 1480 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1472 1481 nullable=True, unique=False, default=None)
1473 1482 group_id = Column(
1474 1483 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1475 1484 unique=False, default=None)
1476 1485
1477 1486 user = relationship('User', lazy='joined')
1478 1487 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1479 1488 group = relationship('RepoGroup', lazy='joined')
1480 1489 repo_to_perm = relationship(
1481 1490 'UserRepoToPerm', cascade='all',
1482 1491 order_by='UserRepoToPerm.repo_to_perm_id')
1483 1492 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1484 1493 stats = relationship('Statistics', cascade='all', uselist=False)
1485 1494
1486 1495 followers = relationship(
1487 1496 'UserFollowing',
1488 1497 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1489 1498 cascade='all')
1490 1499 extra_fields = relationship(
1491 1500 'RepositoryField', cascade="all, delete, delete-orphan")
1492 1501 logs = relationship('UserLog')
1493 1502 comments = relationship(
1494 1503 'ChangesetComment', cascade="all, delete, delete-orphan")
1495 1504 pull_requests_source = relationship(
1496 1505 'PullRequest',
1497 1506 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1498 1507 cascade="all, delete, delete-orphan")
1499 1508 pull_requests_target = relationship(
1500 1509 'PullRequest',
1501 1510 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1502 1511 cascade="all, delete, delete-orphan")
1503 1512 ui = relationship('RepoRhodeCodeUi', cascade="all")
1504 1513 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1505 1514 integrations = relationship('Integration',
1506 1515 cascade="all, delete, delete-orphan")
1507 1516
1508 1517 def __unicode__(self):
1509 1518 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1510 1519 safe_unicode(self.repo_name))
1511 1520
1512 1521 @hybrid_property
1513 1522 def description_safe(self):
1514 1523 from rhodecode.lib import helpers as h
1515 1524 return h.escape(self.description)
1516 1525
1517 1526 @hybrid_property
1518 1527 def landing_rev(self):
1519 1528 # always should return [rev_type, rev]
1520 1529 if self._landing_revision:
1521 1530 _rev_info = self._landing_revision.split(':')
1522 1531 if len(_rev_info) < 2:
1523 1532 _rev_info.insert(0, 'rev')
1524 1533 return [_rev_info[0], _rev_info[1]]
1525 1534 return [None, None]
1526 1535
1527 1536 @landing_rev.setter
1528 1537 def landing_rev(self, val):
1529 1538 if ':' not in val:
1530 1539 raise ValueError('value must be delimited with `:` and consist '
1531 1540 'of <rev_type>:<rev>, got %s instead' % val)
1532 1541 self._landing_revision = val
1533 1542
1534 1543 @hybrid_property
1535 1544 def locked(self):
1536 1545 if self._locked:
1537 1546 user_id, timelocked, reason = self._locked.split(':')
1538 1547 lock_values = int(user_id), timelocked, reason
1539 1548 else:
1540 1549 lock_values = [None, None, None]
1541 1550 return lock_values
1542 1551
1543 1552 @locked.setter
1544 1553 def locked(self, val):
1545 1554 if val and isinstance(val, (list, tuple)):
1546 1555 self._locked = ':'.join(map(str, val))
1547 1556 else:
1548 1557 self._locked = None
1549 1558
1550 1559 @hybrid_property
1551 1560 def changeset_cache(self):
1552 1561 from rhodecode.lib.vcs.backends.base import EmptyCommit
1553 1562 dummy = EmptyCommit().__json__()
1554 1563 if not self._changeset_cache:
1555 1564 return dummy
1556 1565 try:
1557 1566 return json.loads(self._changeset_cache)
1558 1567 except TypeError:
1559 1568 return dummy
1560 1569 except Exception:
1561 1570 log.error(traceback.format_exc())
1562 1571 return dummy
1563 1572
1564 1573 @changeset_cache.setter
1565 1574 def changeset_cache(self, val):
1566 1575 try:
1567 1576 self._changeset_cache = json.dumps(val)
1568 1577 except Exception:
1569 1578 log.error(traceback.format_exc())
1570 1579
1571 1580 @hybrid_property
1572 1581 def repo_name(self):
1573 1582 return self._repo_name
1574 1583
1575 1584 @repo_name.setter
1576 1585 def repo_name(self, value):
1577 1586 self._repo_name = value
1578 1587 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1579 1588
1580 1589 @classmethod
1581 1590 def normalize_repo_name(cls, repo_name):
1582 1591 """
1583 1592 Normalizes os specific repo_name to the format internally stored inside
1584 1593 database using URL_SEP
1585 1594
1586 1595 :param cls:
1587 1596 :param repo_name:
1588 1597 """
1589 1598 return cls.NAME_SEP.join(repo_name.split(os.sep))
1590 1599
1591 1600 @classmethod
1592 1601 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1593 1602 session = Session()
1594 1603 q = session.query(cls).filter(cls.repo_name == repo_name)
1595 1604
1596 1605 if cache:
1597 1606 if identity_cache:
1598 1607 val = cls.identity_cache(session, 'repo_name', repo_name)
1599 1608 if val:
1600 1609 return val
1601 1610 else:
1602 1611 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1603 1612 q = q.options(
1604 1613 FromCache("sql_cache_short", cache_key))
1605 1614
1606 1615 return q.scalar()
1607 1616
1608 1617 @classmethod
1609 1618 def get_by_full_path(cls, repo_full_path):
1610 1619 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1611 1620 repo_name = cls.normalize_repo_name(repo_name)
1612 1621 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1613 1622
1614 1623 @classmethod
1615 1624 def get_repo_forks(cls, repo_id):
1616 1625 return cls.query().filter(Repository.fork_id == repo_id)
1617 1626
1618 1627 @classmethod
1619 1628 def base_path(cls):
1620 1629 """
1621 1630 Returns base path when all repos are stored
1622 1631
1623 1632 :param cls:
1624 1633 """
1625 1634 q = Session().query(RhodeCodeUi)\
1626 1635 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1627 1636 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1628 1637 return q.one().ui_value
1629 1638
1630 1639 @classmethod
1631 1640 def is_valid(cls, repo_name):
1632 1641 """
1633 1642 returns True if given repo name is a valid filesystem repository
1634 1643
1635 1644 :param cls:
1636 1645 :param repo_name:
1637 1646 """
1638 1647 from rhodecode.lib.utils import is_valid_repo
1639 1648
1640 1649 return is_valid_repo(repo_name, cls.base_path())
1641 1650
1642 1651 @classmethod
1643 1652 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1644 1653 case_insensitive=True):
1645 1654 q = Repository.query()
1646 1655
1647 1656 if not isinstance(user_id, Optional):
1648 1657 q = q.filter(Repository.user_id == user_id)
1649 1658
1650 1659 if not isinstance(group_id, Optional):
1651 1660 q = q.filter(Repository.group_id == group_id)
1652 1661
1653 1662 if case_insensitive:
1654 1663 q = q.order_by(func.lower(Repository.repo_name))
1655 1664 else:
1656 1665 q = q.order_by(Repository.repo_name)
1657 1666 return q.all()
1658 1667
1659 1668 @property
1660 1669 def forks(self):
1661 1670 """
1662 1671 Return forks of this repo
1663 1672 """
1664 1673 return Repository.get_repo_forks(self.repo_id)
1665 1674
1666 1675 @property
1667 1676 def parent(self):
1668 1677 """
1669 1678 Returns fork parent
1670 1679 """
1671 1680 return self.fork
1672 1681
1673 1682 @property
1674 1683 def just_name(self):
1675 1684 return self.repo_name.split(self.NAME_SEP)[-1]
1676 1685
1677 1686 @property
1678 1687 def groups_with_parents(self):
1679 1688 groups = []
1680 1689 if self.group is None:
1681 1690 return groups
1682 1691
1683 1692 cur_gr = self.group
1684 1693 groups.insert(0, cur_gr)
1685 1694 while 1:
1686 1695 gr = getattr(cur_gr, 'parent_group', None)
1687 1696 cur_gr = cur_gr.parent_group
1688 1697 if gr is None:
1689 1698 break
1690 1699 groups.insert(0, gr)
1691 1700
1692 1701 return groups
1693 1702
1694 1703 @property
1695 1704 def groups_and_repo(self):
1696 1705 return self.groups_with_parents, self
1697 1706
1698 1707 @LazyProperty
1699 1708 def repo_path(self):
1700 1709 """
1701 1710 Returns base full path for that repository means where it actually
1702 1711 exists on a filesystem
1703 1712 """
1704 1713 q = Session().query(RhodeCodeUi).filter(
1705 1714 RhodeCodeUi.ui_key == self.NAME_SEP)
1706 1715 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1707 1716 return q.one().ui_value
1708 1717
1709 1718 @property
1710 1719 def repo_full_path(self):
1711 1720 p = [self.repo_path]
1712 1721 # we need to split the name by / since this is how we store the
1713 1722 # names in the database, but that eventually needs to be converted
1714 1723 # into a valid system path
1715 1724 p += self.repo_name.split(self.NAME_SEP)
1716 1725 return os.path.join(*map(safe_unicode, p))
1717 1726
1718 1727 @property
1719 1728 def cache_keys(self):
1720 1729 """
1721 1730 Returns associated cache keys for that repo
1722 1731 """
1723 1732 return CacheKey.query()\
1724 1733 .filter(CacheKey.cache_args == self.repo_name)\
1725 1734 .order_by(CacheKey.cache_key)\
1726 1735 .all()
1727 1736
1728 1737 def get_new_name(self, repo_name):
1729 1738 """
1730 1739 returns new full repository name based on assigned group and new new
1731 1740
1732 1741 :param group_name:
1733 1742 """
1734 1743 path_prefix = self.group.full_path_splitted if self.group else []
1735 1744 return self.NAME_SEP.join(path_prefix + [repo_name])
1736 1745
1737 1746 @property
1738 1747 def _config(self):
1739 1748 """
1740 1749 Returns db based config object.
1741 1750 """
1742 1751 from rhodecode.lib.utils import make_db_config
1743 1752 return make_db_config(clear_session=False, repo=self)
1744 1753
1745 1754 def permissions(self, with_admins=True, with_owner=True):
1746 1755 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1747 1756 q = q.options(joinedload(UserRepoToPerm.repository),
1748 1757 joinedload(UserRepoToPerm.user),
1749 1758 joinedload(UserRepoToPerm.permission),)
1750 1759
1751 1760 # get owners and admins and permissions. We do a trick of re-writing
1752 1761 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1753 1762 # has a global reference and changing one object propagates to all
1754 1763 # others. This means if admin is also an owner admin_row that change
1755 1764 # would propagate to both objects
1756 1765 perm_rows = []
1757 1766 for _usr in q.all():
1758 1767 usr = AttributeDict(_usr.user.get_dict())
1759 1768 usr.permission = _usr.permission.permission_name
1760 1769 perm_rows.append(usr)
1761 1770
1762 1771 # filter the perm rows by 'default' first and then sort them by
1763 1772 # admin,write,read,none permissions sorted again alphabetically in
1764 1773 # each group
1765 1774 perm_rows = sorted(perm_rows, key=display_sort)
1766 1775
1767 1776 _admin_perm = 'repository.admin'
1768 1777 owner_row = []
1769 1778 if with_owner:
1770 1779 usr = AttributeDict(self.user.get_dict())
1771 1780 usr.owner_row = True
1772 1781 usr.permission = _admin_perm
1773 1782 owner_row.append(usr)
1774 1783
1775 1784 super_admin_rows = []
1776 1785 if with_admins:
1777 1786 for usr in User.get_all_super_admins():
1778 1787 # if this admin is also owner, don't double the record
1779 1788 if usr.user_id == owner_row[0].user_id:
1780 1789 owner_row[0].admin_row = True
1781 1790 else:
1782 1791 usr = AttributeDict(usr.get_dict())
1783 1792 usr.admin_row = True
1784 1793 usr.permission = _admin_perm
1785 1794 super_admin_rows.append(usr)
1786 1795
1787 1796 return super_admin_rows + owner_row + perm_rows
1788 1797
1789 1798 def permission_user_groups(self):
1790 1799 q = UserGroupRepoToPerm.query().filter(
1791 1800 UserGroupRepoToPerm.repository == self)
1792 1801 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1793 1802 joinedload(UserGroupRepoToPerm.users_group),
1794 1803 joinedload(UserGroupRepoToPerm.permission),)
1795 1804
1796 1805 perm_rows = []
1797 1806 for _user_group in q.all():
1798 1807 usr = AttributeDict(_user_group.users_group.get_dict())
1799 1808 usr.permission = _user_group.permission.permission_name
1800 1809 perm_rows.append(usr)
1801 1810
1802 1811 return perm_rows
1803 1812
1804 1813 def get_api_data(self, include_secrets=False):
1805 1814 """
1806 1815 Common function for generating repo api data
1807 1816
1808 1817 :param include_secrets: See :meth:`User.get_api_data`.
1809 1818
1810 1819 """
1811 1820 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1812 1821 # move this methods on models level.
1813 1822 from rhodecode.model.settings import SettingsModel
1814 1823 from rhodecode.model.repo import RepoModel
1815 1824
1816 1825 repo = self
1817 1826 _user_id, _time, _reason = self.locked
1818 1827
1819 1828 data = {
1820 1829 'repo_id': repo.repo_id,
1821 1830 'repo_name': repo.repo_name,
1822 1831 'repo_type': repo.repo_type,
1823 1832 'clone_uri': repo.clone_uri or '',
1824 1833 'url': RepoModel().get_url(self),
1825 1834 'private': repo.private,
1826 1835 'created_on': repo.created_on,
1827 1836 'description': repo.description_safe,
1828 1837 'landing_rev': repo.landing_rev,
1829 1838 'owner': repo.user.username,
1830 1839 'fork_of': repo.fork.repo_name if repo.fork else None,
1831 1840 'fork_of_id': repo.fork.repo_id if repo.fork else None,
1832 1841 'enable_statistics': repo.enable_statistics,
1833 1842 'enable_locking': repo.enable_locking,
1834 1843 'enable_downloads': repo.enable_downloads,
1835 1844 'last_changeset': repo.changeset_cache,
1836 1845 'locked_by': User.get(_user_id).get_api_data(
1837 1846 include_secrets=include_secrets) if _user_id else None,
1838 1847 'locked_date': time_to_datetime(_time) if _time else None,
1839 1848 'lock_reason': _reason if _reason else None,
1840 1849 }
1841 1850
1842 1851 # TODO: mikhail: should be per-repo settings here
1843 1852 rc_config = SettingsModel().get_all_settings()
1844 1853 repository_fields = str2bool(
1845 1854 rc_config.get('rhodecode_repository_fields'))
1846 1855 if repository_fields:
1847 1856 for f in self.extra_fields:
1848 1857 data[f.field_key_prefixed] = f.field_value
1849 1858
1850 1859 return data
1851 1860
1852 1861 @classmethod
1853 1862 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1854 1863 if not lock_time:
1855 1864 lock_time = time.time()
1856 1865 if not lock_reason:
1857 1866 lock_reason = cls.LOCK_AUTOMATIC
1858 1867 repo.locked = [user_id, lock_time, lock_reason]
1859 1868 Session().add(repo)
1860 1869 Session().commit()
1861 1870
1862 1871 @classmethod
1863 1872 def unlock(cls, repo):
1864 1873 repo.locked = None
1865 1874 Session().add(repo)
1866 1875 Session().commit()
1867 1876
1868 1877 @classmethod
1869 1878 def getlock(cls, repo):
1870 1879 return repo.locked
1871 1880
1872 1881 def is_user_lock(self, user_id):
1873 1882 if self.lock[0]:
1874 1883 lock_user_id = safe_int(self.lock[0])
1875 1884 user_id = safe_int(user_id)
1876 1885 # both are ints, and they are equal
1877 1886 return all([lock_user_id, user_id]) and lock_user_id == user_id
1878 1887
1879 1888 return False
1880 1889
1881 1890 def get_locking_state(self, action, user_id, only_when_enabled=True):
1882 1891 """
1883 1892 Checks locking on this repository, if locking is enabled and lock is
1884 1893 present returns a tuple of make_lock, locked, locked_by.
1885 1894 make_lock can have 3 states None (do nothing) True, make lock
1886 1895 False release lock, This value is later propagated to hooks, which
1887 1896 do the locking. Think about this as signals passed to hooks what to do.
1888 1897
1889 1898 """
1890 1899 # TODO: johbo: This is part of the business logic and should be moved
1891 1900 # into the RepositoryModel.
1892 1901
1893 1902 if action not in ('push', 'pull'):
1894 1903 raise ValueError("Invalid action value: %s" % repr(action))
1895 1904
1896 1905 # defines if locked error should be thrown to user
1897 1906 currently_locked = False
1898 1907 # defines if new lock should be made, tri-state
1899 1908 make_lock = None
1900 1909 repo = self
1901 1910 user = User.get(user_id)
1902 1911
1903 1912 lock_info = repo.locked
1904 1913
1905 1914 if repo and (repo.enable_locking or not only_when_enabled):
1906 1915 if action == 'push':
1907 1916 # check if it's already locked !, if it is compare users
1908 1917 locked_by_user_id = lock_info[0]
1909 1918 if user.user_id == locked_by_user_id:
1910 1919 log.debug(
1911 1920 'Got `push` action from user %s, now unlocking', user)
1912 1921 # unlock if we have push from user who locked
1913 1922 make_lock = False
1914 1923 else:
1915 1924 # we're not the same user who locked, ban with
1916 1925 # code defined in settings (default is 423 HTTP Locked) !
1917 1926 log.debug('Repo %s is currently locked by %s', repo, user)
1918 1927 currently_locked = True
1919 1928 elif action == 'pull':
1920 1929 # [0] user [1] date
1921 1930 if lock_info[0] and lock_info[1]:
1922 1931 log.debug('Repo %s is currently locked by %s', repo, user)
1923 1932 currently_locked = True
1924 1933 else:
1925 1934 log.debug('Setting lock on repo %s by %s', repo, user)
1926 1935 make_lock = True
1927 1936
1928 1937 else:
1929 1938 log.debug('Repository %s do not have locking enabled', repo)
1930 1939
1931 1940 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1932 1941 make_lock, currently_locked, lock_info)
1933 1942
1934 1943 from rhodecode.lib.auth import HasRepoPermissionAny
1935 1944 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1936 1945 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1937 1946 # if we don't have at least write permission we cannot make a lock
1938 1947 log.debug('lock state reset back to FALSE due to lack '
1939 1948 'of at least read permission')
1940 1949 make_lock = False
1941 1950
1942 1951 return make_lock, currently_locked, lock_info
1943 1952
1944 1953 @property
1945 1954 def last_db_change(self):
1946 1955 return self.updated_on
1947 1956
1948 1957 @property
1949 1958 def clone_uri_hidden(self):
1950 1959 clone_uri = self.clone_uri
1951 1960 if clone_uri:
1952 1961 import urlobject
1953 1962 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
1954 1963 if url_obj.password:
1955 1964 clone_uri = url_obj.with_password('*****')
1956 1965 return clone_uri
1957 1966
1958 1967 def clone_url(self, **override):
1959 1968 from rhodecode.model.settings import SettingsModel
1960 1969
1961 1970 uri_tmpl = None
1962 1971 if 'with_id' in override:
1963 1972 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1964 1973 del override['with_id']
1965 1974
1966 1975 if 'uri_tmpl' in override:
1967 1976 uri_tmpl = override['uri_tmpl']
1968 1977 del override['uri_tmpl']
1969 1978
1970 1979 # we didn't override our tmpl from **overrides
1971 1980 if not uri_tmpl:
1972 1981 rc_config = SettingsModel().get_all_settings(cache=True)
1973 1982 uri_tmpl = rc_config.get(
1974 1983 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
1975 1984
1976 1985 request = get_current_request()
1977 1986 return get_clone_url(request=request,
1978 1987 uri_tmpl=uri_tmpl,
1979 1988 repo_name=self.repo_name,
1980 1989 repo_id=self.repo_id, **override)
1981 1990
1982 1991 def set_state(self, state):
1983 1992 self.repo_state = state
1984 1993 Session().add(self)
1985 1994 #==========================================================================
1986 1995 # SCM PROPERTIES
1987 1996 #==========================================================================
1988 1997
1989 1998 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1990 1999 return get_commit_safe(
1991 2000 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1992 2001
1993 2002 def get_changeset(self, rev=None, pre_load=None):
1994 2003 warnings.warn("Use get_commit", DeprecationWarning)
1995 2004 commit_id = None
1996 2005 commit_idx = None
1997 2006 if isinstance(rev, basestring):
1998 2007 commit_id = rev
1999 2008 else:
2000 2009 commit_idx = rev
2001 2010 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2002 2011 pre_load=pre_load)
2003 2012
2004 2013 def get_landing_commit(self):
2005 2014 """
2006 2015 Returns landing commit, or if that doesn't exist returns the tip
2007 2016 """
2008 2017 _rev_type, _rev = self.landing_rev
2009 2018 commit = self.get_commit(_rev)
2010 2019 if isinstance(commit, EmptyCommit):
2011 2020 return self.get_commit()
2012 2021 return commit
2013 2022
2014 2023 def update_commit_cache(self, cs_cache=None, config=None):
2015 2024 """
2016 2025 Update cache of last changeset for repository, keys should be::
2017 2026
2018 2027 short_id
2019 2028 raw_id
2020 2029 revision
2021 2030 parents
2022 2031 message
2023 2032 date
2024 2033 author
2025 2034
2026 2035 :param cs_cache:
2027 2036 """
2028 2037 from rhodecode.lib.vcs.backends.base import BaseChangeset
2029 2038 if cs_cache is None:
2030 2039 # use no-cache version here
2031 2040 scm_repo = self.scm_instance(cache=False, config=config)
2032 2041 if scm_repo:
2033 2042 cs_cache = scm_repo.get_commit(
2034 2043 pre_load=["author", "date", "message", "parents"])
2035 2044 else:
2036 2045 cs_cache = EmptyCommit()
2037 2046
2038 2047 if isinstance(cs_cache, BaseChangeset):
2039 2048 cs_cache = cs_cache.__json__()
2040 2049
2041 2050 def is_outdated(new_cs_cache):
2042 2051 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2043 2052 new_cs_cache['revision'] != self.changeset_cache['revision']):
2044 2053 return True
2045 2054 return False
2046 2055
2047 2056 # check if we have maybe already latest cached revision
2048 2057 if is_outdated(cs_cache) or not self.changeset_cache:
2049 2058 _default = datetime.datetime.fromtimestamp(0)
2050 2059 last_change = cs_cache.get('date') or _default
2051 2060 log.debug('updated repo %s with new cs cache %s',
2052 2061 self.repo_name, cs_cache)
2053 2062 self.updated_on = last_change
2054 2063 self.changeset_cache = cs_cache
2055 2064 Session().add(self)
2056 2065 Session().commit()
2057 2066 else:
2058 2067 log.debug('Skipping update_commit_cache for repo:`%s` '
2059 2068 'commit already with latest changes', self.repo_name)
2060 2069
2061 2070 @property
2062 2071 def tip(self):
2063 2072 return self.get_commit('tip')
2064 2073
2065 2074 @property
2066 2075 def author(self):
2067 2076 return self.tip.author
2068 2077
2069 2078 @property
2070 2079 def last_change(self):
2071 2080 return self.scm_instance().last_change
2072 2081
2073 2082 def get_comments(self, revisions=None):
2074 2083 """
2075 2084 Returns comments for this repository grouped by revisions
2076 2085
2077 2086 :param revisions: filter query by revisions only
2078 2087 """
2079 2088 cmts = ChangesetComment.query()\
2080 2089 .filter(ChangesetComment.repo == self)
2081 2090 if revisions:
2082 2091 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2083 2092 grouped = collections.defaultdict(list)
2084 2093 for cmt in cmts.all():
2085 2094 grouped[cmt.revision].append(cmt)
2086 2095 return grouped
2087 2096
2088 2097 def statuses(self, revisions=None):
2089 2098 """
2090 2099 Returns statuses for this repository
2091 2100
2092 2101 :param revisions: list of revisions to get statuses for
2093 2102 """
2094 2103 statuses = ChangesetStatus.query()\
2095 2104 .filter(ChangesetStatus.repo == self)\
2096 2105 .filter(ChangesetStatus.version == 0)
2097 2106
2098 2107 if revisions:
2099 2108 # Try doing the filtering in chunks to avoid hitting limits
2100 2109 size = 500
2101 2110 status_results = []
2102 2111 for chunk in xrange(0, len(revisions), size):
2103 2112 status_results += statuses.filter(
2104 2113 ChangesetStatus.revision.in_(
2105 2114 revisions[chunk: chunk+size])
2106 2115 ).all()
2107 2116 else:
2108 2117 status_results = statuses.all()
2109 2118
2110 2119 grouped = {}
2111 2120
2112 2121 # maybe we have open new pullrequest without a status?
2113 2122 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2114 2123 status_lbl = ChangesetStatus.get_status_lbl(stat)
2115 2124 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2116 2125 for rev in pr.revisions:
2117 2126 pr_id = pr.pull_request_id
2118 2127 pr_repo = pr.target_repo.repo_name
2119 2128 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2120 2129
2121 2130 for stat in status_results:
2122 2131 pr_id = pr_repo = None
2123 2132 if stat.pull_request:
2124 2133 pr_id = stat.pull_request.pull_request_id
2125 2134 pr_repo = stat.pull_request.target_repo.repo_name
2126 2135 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2127 2136 pr_id, pr_repo]
2128 2137 return grouped
2129 2138
2130 2139 # ==========================================================================
2131 2140 # SCM CACHE INSTANCE
2132 2141 # ==========================================================================
2133 2142
2134 2143 def scm_instance(self, **kwargs):
2135 2144 import rhodecode
2136 2145
2137 2146 # Passing a config will not hit the cache currently only used
2138 2147 # for repo2dbmapper
2139 2148 config = kwargs.pop('config', None)
2140 2149 cache = kwargs.pop('cache', None)
2141 2150 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2142 2151 # if cache is NOT defined use default global, else we have a full
2143 2152 # control over cache behaviour
2144 2153 if cache is None and full_cache and not config:
2145 2154 return self._get_instance_cached()
2146 2155 return self._get_instance(cache=bool(cache), config=config)
2147 2156
2148 2157 def _get_instance_cached(self):
2149 2158 @cache_region('long_term')
2150 2159 def _get_repo(cache_key):
2151 2160 return self._get_instance()
2152 2161
2153 2162 invalidator_context = CacheKey.repo_context_cache(
2154 2163 _get_repo, self.repo_name, None, thread_scoped=True)
2155 2164
2156 2165 with invalidator_context as context:
2157 2166 context.invalidate()
2158 2167 repo = context.compute()
2159 2168
2160 2169 return repo
2161 2170
2162 2171 def _get_instance(self, cache=True, config=None):
2163 2172 config = config or self._config
2164 2173 custom_wire = {
2165 2174 'cache': cache # controls the vcs.remote cache
2166 2175 }
2167 2176 repo = get_vcs_instance(
2168 2177 repo_path=safe_str(self.repo_full_path),
2169 2178 config=config,
2170 2179 with_wire=custom_wire,
2171 2180 create=False,
2172 2181 _vcs_alias=self.repo_type)
2173 2182
2174 2183 return repo
2175 2184
2176 2185 def __json__(self):
2177 2186 return {'landing_rev': self.landing_rev}
2178 2187
2179 2188 def get_dict(self):
2180 2189
2181 2190 # Since we transformed `repo_name` to a hybrid property, we need to
2182 2191 # keep compatibility with the code which uses `repo_name` field.
2183 2192
2184 2193 result = super(Repository, self).get_dict()
2185 2194 result['repo_name'] = result.pop('_repo_name', None)
2186 2195 return result
2187 2196
2188 2197
2189 2198 class RepoGroup(Base, BaseModel):
2190 2199 __tablename__ = 'groups'
2191 2200 __table_args__ = (
2192 2201 UniqueConstraint('group_name', 'group_parent_id'),
2193 2202 CheckConstraint('group_id != group_parent_id'),
2194 2203 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2195 2204 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2196 2205 )
2197 2206 __mapper_args__ = {'order_by': 'group_name'}
2198 2207
2199 2208 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2200 2209
2201 2210 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2202 2211 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2203 2212 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2204 2213 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2205 2214 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2206 2215 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2207 2216 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2208 2217 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2209 2218 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2210 2219
2211 2220 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2212 2221 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2213 2222 parent_group = relationship('RepoGroup', remote_side=group_id)
2214 2223 user = relationship('User')
2215 2224 integrations = relationship('Integration',
2216 2225 cascade="all, delete, delete-orphan")
2217 2226
2218 2227 def __init__(self, group_name='', parent_group=None):
2219 2228 self.group_name = group_name
2220 2229 self.parent_group = parent_group
2221 2230
2222 2231 def __unicode__(self):
2223 2232 return u"<%s('id:%s:%s')>" % (
2224 2233 self.__class__.__name__, self.group_id, self.group_name)
2225 2234
2226 2235 @hybrid_property
2227 2236 def description_safe(self):
2228 2237 from rhodecode.lib import helpers as h
2229 2238 return h.escape(self.group_description)
2230 2239
2231 2240 @classmethod
2232 2241 def _generate_choice(cls, repo_group):
2233 2242 from webhelpers.html import literal as _literal
2234 2243 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2235 2244 return repo_group.group_id, _name(repo_group.full_path_splitted)
2236 2245
2237 2246 @classmethod
2238 2247 def groups_choices(cls, groups=None, show_empty_group=True):
2239 2248 if not groups:
2240 2249 groups = cls.query().all()
2241 2250
2242 2251 repo_groups = []
2243 2252 if show_empty_group:
2244 2253 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2245 2254
2246 2255 repo_groups.extend([cls._generate_choice(x) for x in groups])
2247 2256
2248 2257 repo_groups = sorted(
2249 2258 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2250 2259 return repo_groups
2251 2260
2252 2261 @classmethod
2253 2262 def url_sep(cls):
2254 2263 return URL_SEP
2255 2264
2256 2265 @classmethod
2257 2266 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2258 2267 if case_insensitive:
2259 2268 gr = cls.query().filter(func.lower(cls.group_name)
2260 2269 == func.lower(group_name))
2261 2270 else:
2262 2271 gr = cls.query().filter(cls.group_name == group_name)
2263 2272 if cache:
2264 2273 name_key = _hash_key(group_name)
2265 2274 gr = gr.options(
2266 2275 FromCache("sql_cache_short", "get_group_%s" % name_key))
2267 2276 return gr.scalar()
2268 2277
2269 2278 @classmethod
2270 2279 def get_user_personal_repo_group(cls, user_id):
2271 2280 user = User.get(user_id)
2272 2281 if user.username == User.DEFAULT_USER:
2273 2282 return None
2274 2283
2275 2284 return cls.query()\
2276 2285 .filter(cls.personal == true()) \
2277 2286 .filter(cls.user == user).scalar()
2278 2287
2279 2288 @classmethod
2280 2289 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2281 2290 case_insensitive=True):
2282 2291 q = RepoGroup.query()
2283 2292
2284 2293 if not isinstance(user_id, Optional):
2285 2294 q = q.filter(RepoGroup.user_id == user_id)
2286 2295
2287 2296 if not isinstance(group_id, Optional):
2288 2297 q = q.filter(RepoGroup.group_parent_id == group_id)
2289 2298
2290 2299 if case_insensitive:
2291 2300 q = q.order_by(func.lower(RepoGroup.group_name))
2292 2301 else:
2293 2302 q = q.order_by(RepoGroup.group_name)
2294 2303 return q.all()
2295 2304
2296 2305 @property
2297 2306 def parents(self):
2298 2307 parents_recursion_limit = 10
2299 2308 groups = []
2300 2309 if self.parent_group is None:
2301 2310 return groups
2302 2311 cur_gr = self.parent_group
2303 2312 groups.insert(0, cur_gr)
2304 2313 cnt = 0
2305 2314 while 1:
2306 2315 cnt += 1
2307 2316 gr = getattr(cur_gr, 'parent_group', None)
2308 2317 cur_gr = cur_gr.parent_group
2309 2318 if gr is None:
2310 2319 break
2311 2320 if cnt == parents_recursion_limit:
2312 2321 # this will prevent accidental infinit loops
2313 2322 log.error(('more than %s parents found for group %s, stopping '
2314 2323 'recursive parent fetching' % (parents_recursion_limit, self)))
2315 2324 break
2316 2325
2317 2326 groups.insert(0, gr)
2318 2327 return groups
2319 2328
2320 2329 @property
2321 2330 def last_db_change(self):
2322 2331 return self.updated_on
2323 2332
2324 2333 @property
2325 2334 def children(self):
2326 2335 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2327 2336
2328 2337 @property
2329 2338 def name(self):
2330 2339 return self.group_name.split(RepoGroup.url_sep())[-1]
2331 2340
2332 2341 @property
2333 2342 def full_path(self):
2334 2343 return self.group_name
2335 2344
2336 2345 @property
2337 2346 def full_path_splitted(self):
2338 2347 return self.group_name.split(RepoGroup.url_sep())
2339 2348
2340 2349 @property
2341 2350 def repositories(self):
2342 2351 return Repository.query()\
2343 2352 .filter(Repository.group == self)\
2344 2353 .order_by(Repository.repo_name)
2345 2354
2346 2355 @property
2347 2356 def repositories_recursive_count(self):
2348 2357 cnt = self.repositories.count()
2349 2358
2350 2359 def children_count(group):
2351 2360 cnt = 0
2352 2361 for child in group.children:
2353 2362 cnt += child.repositories.count()
2354 2363 cnt += children_count(child)
2355 2364 return cnt
2356 2365
2357 2366 return cnt + children_count(self)
2358 2367
2359 2368 def _recursive_objects(self, include_repos=True):
2360 2369 all_ = []
2361 2370
2362 2371 def _get_members(root_gr):
2363 2372 if include_repos:
2364 2373 for r in root_gr.repositories:
2365 2374 all_.append(r)
2366 2375 childs = root_gr.children.all()
2367 2376 if childs:
2368 2377 for gr in childs:
2369 2378 all_.append(gr)
2370 2379 _get_members(gr)
2371 2380
2372 2381 _get_members(self)
2373 2382 return [self] + all_
2374 2383
2375 2384 def recursive_groups_and_repos(self):
2376 2385 """
2377 2386 Recursive return all groups, with repositories in those groups
2378 2387 """
2379 2388 return self._recursive_objects()
2380 2389
2381 2390 def recursive_groups(self):
2382 2391 """
2383 2392 Returns all children groups for this group including children of children
2384 2393 """
2385 2394 return self._recursive_objects(include_repos=False)
2386 2395
2387 2396 def get_new_name(self, group_name):
2388 2397 """
2389 2398 returns new full group name based on parent and new name
2390 2399
2391 2400 :param group_name:
2392 2401 """
2393 2402 path_prefix = (self.parent_group.full_path_splitted if
2394 2403 self.parent_group else [])
2395 2404 return RepoGroup.url_sep().join(path_prefix + [group_name])
2396 2405
2397 2406 def permissions(self, with_admins=True, with_owner=True):
2398 2407 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2399 2408 q = q.options(joinedload(UserRepoGroupToPerm.group),
2400 2409 joinedload(UserRepoGroupToPerm.user),
2401 2410 joinedload(UserRepoGroupToPerm.permission),)
2402 2411
2403 2412 # get owners and admins and permissions. We do a trick of re-writing
2404 2413 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2405 2414 # has a global reference and changing one object propagates to all
2406 2415 # others. This means if admin is also an owner admin_row that change
2407 2416 # would propagate to both objects
2408 2417 perm_rows = []
2409 2418 for _usr in q.all():
2410 2419 usr = AttributeDict(_usr.user.get_dict())
2411 2420 usr.permission = _usr.permission.permission_name
2412 2421 perm_rows.append(usr)
2413 2422
2414 2423 # filter the perm rows by 'default' first and then sort them by
2415 2424 # admin,write,read,none permissions sorted again alphabetically in
2416 2425 # each group
2417 2426 perm_rows = sorted(perm_rows, key=display_sort)
2418 2427
2419 2428 _admin_perm = 'group.admin'
2420 2429 owner_row = []
2421 2430 if with_owner:
2422 2431 usr = AttributeDict(self.user.get_dict())
2423 2432 usr.owner_row = True
2424 2433 usr.permission = _admin_perm
2425 2434 owner_row.append(usr)
2426 2435
2427 2436 super_admin_rows = []
2428 2437 if with_admins:
2429 2438 for usr in User.get_all_super_admins():
2430 2439 # if this admin is also owner, don't double the record
2431 2440 if usr.user_id == owner_row[0].user_id:
2432 2441 owner_row[0].admin_row = True
2433 2442 else:
2434 2443 usr = AttributeDict(usr.get_dict())
2435 2444 usr.admin_row = True
2436 2445 usr.permission = _admin_perm
2437 2446 super_admin_rows.append(usr)
2438 2447
2439 2448 return super_admin_rows + owner_row + perm_rows
2440 2449
2441 2450 def permission_user_groups(self):
2442 2451 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2443 2452 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2444 2453 joinedload(UserGroupRepoGroupToPerm.users_group),
2445 2454 joinedload(UserGroupRepoGroupToPerm.permission),)
2446 2455
2447 2456 perm_rows = []
2448 2457 for _user_group in q.all():
2449 2458 usr = AttributeDict(_user_group.users_group.get_dict())
2450 2459 usr.permission = _user_group.permission.permission_name
2451 2460 perm_rows.append(usr)
2452 2461
2453 2462 return perm_rows
2454 2463
2455 2464 def get_api_data(self):
2456 2465 """
2457 2466 Common function for generating api data
2458 2467
2459 2468 """
2460 2469 group = self
2461 2470 data = {
2462 2471 'group_id': group.group_id,
2463 2472 'group_name': group.group_name,
2464 2473 'group_description': group.description_safe,
2465 2474 'parent_group': group.parent_group.group_name if group.parent_group else None,
2466 2475 'repositories': [x.repo_name for x in group.repositories],
2467 2476 'owner': group.user.username,
2468 2477 }
2469 2478 return data
2470 2479
2471 2480
2472 2481 class Permission(Base, BaseModel):
2473 2482 __tablename__ = 'permissions'
2474 2483 __table_args__ = (
2475 2484 Index('p_perm_name_idx', 'permission_name'),
2476 2485 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2477 2486 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2478 2487 )
2479 2488 PERMS = [
2480 2489 ('hg.admin', _('RhodeCode Super Administrator')),
2481 2490
2482 2491 ('repository.none', _('Repository no access')),
2483 2492 ('repository.read', _('Repository read access')),
2484 2493 ('repository.write', _('Repository write access')),
2485 2494 ('repository.admin', _('Repository admin access')),
2486 2495
2487 2496 ('group.none', _('Repository group no access')),
2488 2497 ('group.read', _('Repository group read access')),
2489 2498 ('group.write', _('Repository group write access')),
2490 2499 ('group.admin', _('Repository group admin access')),
2491 2500
2492 2501 ('usergroup.none', _('User group no access')),
2493 2502 ('usergroup.read', _('User group read access')),
2494 2503 ('usergroup.write', _('User group write access')),
2495 2504 ('usergroup.admin', _('User group admin access')),
2496 2505
2497 2506 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2498 2507 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2499 2508
2500 2509 ('hg.usergroup.create.false', _('User Group creation disabled')),
2501 2510 ('hg.usergroup.create.true', _('User Group creation enabled')),
2502 2511
2503 2512 ('hg.create.none', _('Repository creation disabled')),
2504 2513 ('hg.create.repository', _('Repository creation enabled')),
2505 2514 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2506 2515 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2507 2516
2508 2517 ('hg.fork.none', _('Repository forking disabled')),
2509 2518 ('hg.fork.repository', _('Repository forking enabled')),
2510 2519
2511 2520 ('hg.register.none', _('Registration disabled')),
2512 2521 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2513 2522 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2514 2523
2515 2524 ('hg.password_reset.enabled', _('Password reset enabled')),
2516 2525 ('hg.password_reset.hidden', _('Password reset hidden')),
2517 2526 ('hg.password_reset.disabled', _('Password reset disabled')),
2518 2527
2519 2528 ('hg.extern_activate.manual', _('Manual activation of external account')),
2520 2529 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2521 2530
2522 2531 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2523 2532 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2524 2533 ]
2525 2534
2526 2535 # definition of system default permissions for DEFAULT user
2527 2536 DEFAULT_USER_PERMISSIONS = [
2528 2537 'repository.read',
2529 2538 'group.read',
2530 2539 'usergroup.read',
2531 2540 'hg.create.repository',
2532 2541 'hg.repogroup.create.false',
2533 2542 'hg.usergroup.create.false',
2534 2543 'hg.create.write_on_repogroup.true',
2535 2544 'hg.fork.repository',
2536 2545 'hg.register.manual_activate',
2537 2546 'hg.password_reset.enabled',
2538 2547 'hg.extern_activate.auto',
2539 2548 'hg.inherit_default_perms.true',
2540 2549 ]
2541 2550
2542 2551 # defines which permissions are more important higher the more important
2543 2552 # Weight defines which permissions are more important.
2544 2553 # The higher number the more important.
2545 2554 PERM_WEIGHTS = {
2546 2555 'repository.none': 0,
2547 2556 'repository.read': 1,
2548 2557 'repository.write': 3,
2549 2558 'repository.admin': 4,
2550 2559
2551 2560 'group.none': 0,
2552 2561 'group.read': 1,
2553 2562 'group.write': 3,
2554 2563 'group.admin': 4,
2555 2564
2556 2565 'usergroup.none': 0,
2557 2566 'usergroup.read': 1,
2558 2567 'usergroup.write': 3,
2559 2568 'usergroup.admin': 4,
2560 2569
2561 2570 'hg.repogroup.create.false': 0,
2562 2571 'hg.repogroup.create.true': 1,
2563 2572
2564 2573 'hg.usergroup.create.false': 0,
2565 2574 'hg.usergroup.create.true': 1,
2566 2575
2567 2576 'hg.fork.none': 0,
2568 2577 'hg.fork.repository': 1,
2569 2578 'hg.create.none': 0,
2570 2579 'hg.create.repository': 1
2571 2580 }
2572 2581
2573 2582 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2574 2583 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2575 2584 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2576 2585
2577 2586 def __unicode__(self):
2578 2587 return u"<%s('%s:%s')>" % (
2579 2588 self.__class__.__name__, self.permission_id, self.permission_name
2580 2589 )
2581 2590
2582 2591 @classmethod
2583 2592 def get_by_key(cls, key):
2584 2593 return cls.query().filter(cls.permission_name == key).scalar()
2585 2594
2586 2595 @classmethod
2587 2596 def get_default_repo_perms(cls, user_id, repo_id=None):
2588 2597 q = Session().query(UserRepoToPerm, Repository, Permission)\
2589 2598 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2590 2599 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2591 2600 .filter(UserRepoToPerm.user_id == user_id)
2592 2601 if repo_id:
2593 2602 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2594 2603 return q.all()
2595 2604
2596 2605 @classmethod
2597 2606 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2598 2607 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2599 2608 .join(
2600 2609 Permission,
2601 2610 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2602 2611 .join(
2603 2612 Repository,
2604 2613 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2605 2614 .join(
2606 2615 UserGroup,
2607 2616 UserGroupRepoToPerm.users_group_id ==
2608 2617 UserGroup.users_group_id)\
2609 2618 .join(
2610 2619 UserGroupMember,
2611 2620 UserGroupRepoToPerm.users_group_id ==
2612 2621 UserGroupMember.users_group_id)\
2613 2622 .filter(
2614 2623 UserGroupMember.user_id == user_id,
2615 2624 UserGroup.users_group_active == true())
2616 2625 if repo_id:
2617 2626 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2618 2627 return q.all()
2619 2628
2620 2629 @classmethod
2621 2630 def get_default_group_perms(cls, user_id, repo_group_id=None):
2622 2631 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2623 2632 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2624 2633 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2625 2634 .filter(UserRepoGroupToPerm.user_id == user_id)
2626 2635 if repo_group_id:
2627 2636 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2628 2637 return q.all()
2629 2638
2630 2639 @classmethod
2631 2640 def get_default_group_perms_from_user_group(
2632 2641 cls, user_id, repo_group_id=None):
2633 2642 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2634 2643 .join(
2635 2644 Permission,
2636 2645 UserGroupRepoGroupToPerm.permission_id ==
2637 2646 Permission.permission_id)\
2638 2647 .join(
2639 2648 RepoGroup,
2640 2649 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2641 2650 .join(
2642 2651 UserGroup,
2643 2652 UserGroupRepoGroupToPerm.users_group_id ==
2644 2653 UserGroup.users_group_id)\
2645 2654 .join(
2646 2655 UserGroupMember,
2647 2656 UserGroupRepoGroupToPerm.users_group_id ==
2648 2657 UserGroupMember.users_group_id)\
2649 2658 .filter(
2650 2659 UserGroupMember.user_id == user_id,
2651 2660 UserGroup.users_group_active == true())
2652 2661 if repo_group_id:
2653 2662 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2654 2663 return q.all()
2655 2664
2656 2665 @classmethod
2657 2666 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2658 2667 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2659 2668 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2660 2669 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2661 2670 .filter(UserUserGroupToPerm.user_id == user_id)
2662 2671 if user_group_id:
2663 2672 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2664 2673 return q.all()
2665 2674
2666 2675 @classmethod
2667 2676 def get_default_user_group_perms_from_user_group(
2668 2677 cls, user_id, user_group_id=None):
2669 2678 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2670 2679 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2671 2680 .join(
2672 2681 Permission,
2673 2682 UserGroupUserGroupToPerm.permission_id ==
2674 2683 Permission.permission_id)\
2675 2684 .join(
2676 2685 TargetUserGroup,
2677 2686 UserGroupUserGroupToPerm.target_user_group_id ==
2678 2687 TargetUserGroup.users_group_id)\
2679 2688 .join(
2680 2689 UserGroup,
2681 2690 UserGroupUserGroupToPerm.user_group_id ==
2682 2691 UserGroup.users_group_id)\
2683 2692 .join(
2684 2693 UserGroupMember,
2685 2694 UserGroupUserGroupToPerm.user_group_id ==
2686 2695 UserGroupMember.users_group_id)\
2687 2696 .filter(
2688 2697 UserGroupMember.user_id == user_id,
2689 2698 UserGroup.users_group_active == true())
2690 2699 if user_group_id:
2691 2700 q = q.filter(
2692 2701 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2693 2702
2694 2703 return q.all()
2695 2704
2696 2705
2697 2706 class UserRepoToPerm(Base, BaseModel):
2698 2707 __tablename__ = 'repo_to_perm'
2699 2708 __table_args__ = (
2700 2709 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2701 2710 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2702 2711 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2703 2712 )
2704 2713 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2705 2714 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2706 2715 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2707 2716 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2708 2717
2709 2718 user = relationship('User')
2710 2719 repository = relationship('Repository')
2711 2720 permission = relationship('Permission')
2712 2721
2713 2722 @classmethod
2714 2723 def create(cls, user, repository, permission):
2715 2724 n = cls()
2716 2725 n.user = user
2717 2726 n.repository = repository
2718 2727 n.permission = permission
2719 2728 Session().add(n)
2720 2729 return n
2721 2730
2722 2731 def __unicode__(self):
2723 2732 return u'<%s => %s >' % (self.user, self.repository)
2724 2733
2725 2734
2726 2735 class UserUserGroupToPerm(Base, BaseModel):
2727 2736 __tablename__ = 'user_user_group_to_perm'
2728 2737 __table_args__ = (
2729 2738 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2730 2739 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2731 2740 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2732 2741 )
2733 2742 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2734 2743 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2735 2744 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2736 2745 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2737 2746
2738 2747 user = relationship('User')
2739 2748 user_group = relationship('UserGroup')
2740 2749 permission = relationship('Permission')
2741 2750
2742 2751 @classmethod
2743 2752 def create(cls, user, user_group, permission):
2744 2753 n = cls()
2745 2754 n.user = user
2746 2755 n.user_group = user_group
2747 2756 n.permission = permission
2748 2757 Session().add(n)
2749 2758 return n
2750 2759
2751 2760 def __unicode__(self):
2752 2761 return u'<%s => %s >' % (self.user, self.user_group)
2753 2762
2754 2763
2755 2764 class UserToPerm(Base, BaseModel):
2756 2765 __tablename__ = 'user_to_perm'
2757 2766 __table_args__ = (
2758 2767 UniqueConstraint('user_id', 'permission_id'),
2759 2768 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2760 2769 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2761 2770 )
2762 2771 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2763 2772 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2764 2773 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2765 2774
2766 2775 user = relationship('User')
2767 2776 permission = relationship('Permission', lazy='joined')
2768 2777
2769 2778 def __unicode__(self):
2770 2779 return u'<%s => %s >' % (self.user, self.permission)
2771 2780
2772 2781
2773 2782 class UserGroupRepoToPerm(Base, BaseModel):
2774 2783 __tablename__ = 'users_group_repo_to_perm'
2775 2784 __table_args__ = (
2776 2785 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2777 2786 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2778 2787 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2779 2788 )
2780 2789 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2781 2790 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2782 2791 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2783 2792 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2784 2793
2785 2794 users_group = relationship('UserGroup')
2786 2795 permission = relationship('Permission')
2787 2796 repository = relationship('Repository')
2788 2797
2789 2798 @classmethod
2790 2799 def create(cls, users_group, repository, permission):
2791 2800 n = cls()
2792 2801 n.users_group = users_group
2793 2802 n.repository = repository
2794 2803 n.permission = permission
2795 2804 Session().add(n)
2796 2805 return n
2797 2806
2798 2807 def __unicode__(self):
2799 2808 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2800 2809
2801 2810
2802 2811 class UserGroupUserGroupToPerm(Base, BaseModel):
2803 2812 __tablename__ = 'user_group_user_group_to_perm'
2804 2813 __table_args__ = (
2805 2814 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2806 2815 CheckConstraint('target_user_group_id != user_group_id'),
2807 2816 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2808 2817 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2809 2818 )
2810 2819 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2811 2820 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2812 2821 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2813 2822 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2814 2823
2815 2824 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2816 2825 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2817 2826 permission = relationship('Permission')
2818 2827
2819 2828 @classmethod
2820 2829 def create(cls, target_user_group, user_group, permission):
2821 2830 n = cls()
2822 2831 n.target_user_group = target_user_group
2823 2832 n.user_group = user_group
2824 2833 n.permission = permission
2825 2834 Session().add(n)
2826 2835 return n
2827 2836
2828 2837 def __unicode__(self):
2829 2838 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2830 2839
2831 2840
2832 2841 class UserGroupToPerm(Base, BaseModel):
2833 2842 __tablename__ = 'users_group_to_perm'
2834 2843 __table_args__ = (
2835 2844 UniqueConstraint('users_group_id', 'permission_id',),
2836 2845 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2837 2846 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2838 2847 )
2839 2848 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2840 2849 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2841 2850 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2842 2851
2843 2852 users_group = relationship('UserGroup')
2844 2853 permission = relationship('Permission')
2845 2854
2846 2855
2847 2856 class UserRepoGroupToPerm(Base, BaseModel):
2848 2857 __tablename__ = 'user_repo_group_to_perm'
2849 2858 __table_args__ = (
2850 2859 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2851 2860 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2852 2861 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2853 2862 )
2854 2863
2855 2864 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2856 2865 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2857 2866 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2858 2867 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2859 2868
2860 2869 user = relationship('User')
2861 2870 group = relationship('RepoGroup')
2862 2871 permission = relationship('Permission')
2863 2872
2864 2873 @classmethod
2865 2874 def create(cls, user, repository_group, permission):
2866 2875 n = cls()
2867 2876 n.user = user
2868 2877 n.group = repository_group
2869 2878 n.permission = permission
2870 2879 Session().add(n)
2871 2880 return n
2872 2881
2873 2882
2874 2883 class UserGroupRepoGroupToPerm(Base, BaseModel):
2875 2884 __tablename__ = 'users_group_repo_group_to_perm'
2876 2885 __table_args__ = (
2877 2886 UniqueConstraint('users_group_id', 'group_id'),
2878 2887 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2879 2888 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2880 2889 )
2881 2890
2882 2891 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2883 2892 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2884 2893 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2885 2894 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2886 2895
2887 2896 users_group = relationship('UserGroup')
2888 2897 permission = relationship('Permission')
2889 2898 group = relationship('RepoGroup')
2890 2899
2891 2900 @classmethod
2892 2901 def create(cls, user_group, repository_group, permission):
2893 2902 n = cls()
2894 2903 n.users_group = user_group
2895 2904 n.group = repository_group
2896 2905 n.permission = permission
2897 2906 Session().add(n)
2898 2907 return n
2899 2908
2900 2909 def __unicode__(self):
2901 2910 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2902 2911
2903 2912
2904 2913 class Statistics(Base, BaseModel):
2905 2914 __tablename__ = 'statistics'
2906 2915 __table_args__ = (
2907 2916 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2908 2917 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2909 2918 )
2910 2919 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2911 2920 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2912 2921 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2913 2922 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2914 2923 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2915 2924 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2916 2925
2917 2926 repository = relationship('Repository', single_parent=True)
2918 2927
2919 2928
2920 2929 class UserFollowing(Base, BaseModel):
2921 2930 __tablename__ = 'user_followings'
2922 2931 __table_args__ = (
2923 2932 UniqueConstraint('user_id', 'follows_repository_id'),
2924 2933 UniqueConstraint('user_id', 'follows_user_id'),
2925 2934 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2926 2935 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2927 2936 )
2928 2937
2929 2938 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2930 2939 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2931 2940 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2932 2941 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2933 2942 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2934 2943
2935 2944 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2936 2945
2937 2946 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2938 2947 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2939 2948
2940 2949 @classmethod
2941 2950 def get_repo_followers(cls, repo_id):
2942 2951 return cls.query().filter(cls.follows_repo_id == repo_id)
2943 2952
2944 2953
2945 2954 class CacheKey(Base, BaseModel):
2946 2955 __tablename__ = 'cache_invalidation'
2947 2956 __table_args__ = (
2948 2957 UniqueConstraint('cache_key'),
2949 2958 Index('key_idx', 'cache_key'),
2950 2959 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2951 2960 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2952 2961 )
2953 2962 CACHE_TYPE_ATOM = 'ATOM'
2954 2963 CACHE_TYPE_RSS = 'RSS'
2955 2964 CACHE_TYPE_README = 'README'
2956 2965
2957 2966 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2958 2967 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2959 2968 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2960 2969 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2961 2970
2962 2971 def __init__(self, cache_key, cache_args=''):
2963 2972 self.cache_key = cache_key
2964 2973 self.cache_args = cache_args
2965 2974 self.cache_active = False
2966 2975
2967 2976 def __unicode__(self):
2968 2977 return u"<%s('%s:%s[%s]')>" % (
2969 2978 self.__class__.__name__,
2970 2979 self.cache_id, self.cache_key, self.cache_active)
2971 2980
2972 2981 def _cache_key_partition(self):
2973 2982 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2974 2983 return prefix, repo_name, suffix
2975 2984
2976 2985 def get_prefix(self):
2977 2986 """
2978 2987 Try to extract prefix from existing cache key. The key could consist
2979 2988 of prefix, repo_name, suffix
2980 2989 """
2981 2990 # this returns prefix, repo_name, suffix
2982 2991 return self._cache_key_partition()[0]
2983 2992
2984 2993 def get_suffix(self):
2985 2994 """
2986 2995 get suffix that might have been used in _get_cache_key to
2987 2996 generate self.cache_key. Only used for informational purposes
2988 2997 in repo_edit.mako.
2989 2998 """
2990 2999 # prefix, repo_name, suffix
2991 3000 return self._cache_key_partition()[2]
2992 3001
2993 3002 @classmethod
2994 3003 def delete_all_cache(cls):
2995 3004 """
2996 3005 Delete all cache keys from database.
2997 3006 Should only be run when all instances are down and all entries
2998 3007 thus stale.
2999 3008 """
3000 3009 cls.query().delete()
3001 3010 Session().commit()
3002 3011
3003 3012 @classmethod
3004 3013 def get_cache_key(cls, repo_name, cache_type):
3005 3014 """
3006 3015
3007 3016 Generate a cache key for this process of RhodeCode instance.
3008 3017 Prefix most likely will be process id or maybe explicitly set
3009 3018 instance_id from .ini file.
3010 3019 """
3011 3020 import rhodecode
3012 3021 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
3013 3022
3014 3023 repo_as_unicode = safe_unicode(repo_name)
3015 3024 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
3016 3025 if cache_type else repo_as_unicode
3017 3026
3018 3027 return u'{}{}'.format(prefix, key)
3019 3028
3020 3029 @classmethod
3021 3030 def set_invalidate(cls, repo_name, delete=False):
3022 3031 """
3023 3032 Mark all caches of a repo as invalid in the database.
3024 3033 """
3025 3034
3026 3035 try:
3027 3036 qry = Session().query(cls).filter(cls.cache_args == repo_name)
3028 3037 if delete:
3029 3038 log.debug('cache objects deleted for repo %s',
3030 3039 safe_str(repo_name))
3031 3040 qry.delete()
3032 3041 else:
3033 3042 log.debug('cache objects marked as invalid for repo %s',
3034 3043 safe_str(repo_name))
3035 3044 qry.update({"cache_active": False})
3036 3045
3037 3046 Session().commit()
3038 3047 except Exception:
3039 3048 log.exception(
3040 3049 'Cache key invalidation failed for repository %s',
3041 3050 safe_str(repo_name))
3042 3051 Session().rollback()
3043 3052
3044 3053 @classmethod
3045 3054 def get_active_cache(cls, cache_key):
3046 3055 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3047 3056 if inv_obj:
3048 3057 return inv_obj
3049 3058 return None
3050 3059
3051 3060 @classmethod
3052 3061 def repo_context_cache(cls, compute_func, repo_name, cache_type,
3053 3062 thread_scoped=False):
3054 3063 """
3055 3064 @cache_region('long_term')
3056 3065 def _heavy_calculation(cache_key):
3057 3066 return 'result'
3058 3067
3059 3068 cache_context = CacheKey.repo_context_cache(
3060 3069 _heavy_calculation, repo_name, cache_type)
3061 3070
3062 3071 with cache_context as context:
3063 3072 context.invalidate()
3064 3073 computed = context.compute()
3065 3074
3066 3075 assert computed == 'result'
3067 3076 """
3068 3077 from rhodecode.lib import caches
3069 3078 return caches.InvalidationContext(
3070 3079 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
3071 3080
3072 3081
3073 3082 class ChangesetComment(Base, BaseModel):
3074 3083 __tablename__ = 'changeset_comments'
3075 3084 __table_args__ = (
3076 3085 Index('cc_revision_idx', 'revision'),
3077 3086 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3078 3087 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3079 3088 )
3080 3089
3081 3090 COMMENT_OUTDATED = u'comment_outdated'
3082 3091 COMMENT_TYPE_NOTE = u'note'
3083 3092 COMMENT_TYPE_TODO = u'todo'
3084 3093 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3085 3094
3086 3095 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3087 3096 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3088 3097 revision = Column('revision', String(40), nullable=True)
3089 3098 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3090 3099 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3091 3100 line_no = Column('line_no', Unicode(10), nullable=True)
3092 3101 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3093 3102 f_path = Column('f_path', Unicode(1000), nullable=True)
3094 3103 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3095 3104 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3096 3105 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3097 3106 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3098 3107 renderer = Column('renderer', Unicode(64), nullable=True)
3099 3108 display_state = Column('display_state', Unicode(128), nullable=True)
3100 3109
3101 3110 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3102 3111 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3103 3112 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3104 3113 author = relationship('User', lazy='joined')
3105 3114 repo = relationship('Repository')
3106 3115 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3107 3116 pull_request = relationship('PullRequest', lazy='joined')
3108 3117 pull_request_version = relationship('PullRequestVersion')
3109 3118
3110 3119 @classmethod
3111 3120 def get_users(cls, revision=None, pull_request_id=None):
3112 3121 """
3113 3122 Returns user associated with this ChangesetComment. ie those
3114 3123 who actually commented
3115 3124
3116 3125 :param cls:
3117 3126 :param revision:
3118 3127 """
3119 3128 q = Session().query(User)\
3120 3129 .join(ChangesetComment.author)
3121 3130 if revision:
3122 3131 q = q.filter(cls.revision == revision)
3123 3132 elif pull_request_id:
3124 3133 q = q.filter(cls.pull_request_id == pull_request_id)
3125 3134 return q.all()
3126 3135
3127 3136 @classmethod
3128 3137 def get_index_from_version(cls, pr_version, versions):
3129 3138 num_versions = [x.pull_request_version_id for x in versions]
3130 3139 try:
3131 3140 return num_versions.index(pr_version) +1
3132 3141 except (IndexError, ValueError):
3133 3142 return
3134 3143
3135 3144 @property
3136 3145 def outdated(self):
3137 3146 return self.display_state == self.COMMENT_OUTDATED
3138 3147
3139 3148 def outdated_at_version(self, version):
3140 3149 """
3141 3150 Checks if comment is outdated for given pull request version
3142 3151 """
3143 3152 return self.outdated and self.pull_request_version_id != version
3144 3153
3145 3154 def older_than_version(self, version):
3146 3155 """
3147 3156 Checks if comment is made from previous version than given
3148 3157 """
3149 3158 if version is None:
3150 3159 return self.pull_request_version_id is not None
3151 3160
3152 3161 return self.pull_request_version_id < version
3153 3162
3154 3163 @property
3155 3164 def resolved(self):
3156 3165 return self.resolved_by[0] if self.resolved_by else None
3157 3166
3158 3167 @property
3159 3168 def is_todo(self):
3160 3169 return self.comment_type == self.COMMENT_TYPE_TODO
3161 3170
3162 3171 @property
3163 3172 def is_inline(self):
3164 3173 return self.line_no and self.f_path
3165 3174
3166 3175 def get_index_version(self, versions):
3167 3176 return self.get_index_from_version(
3168 3177 self.pull_request_version_id, versions)
3169 3178
3170 3179 def __repr__(self):
3171 3180 if self.comment_id:
3172 3181 return '<DB:Comment #%s>' % self.comment_id
3173 3182 else:
3174 3183 return '<DB:Comment at %#x>' % id(self)
3175 3184
3176 3185 def get_api_data(self):
3177 3186 comment = self
3178 3187 data = {
3179 3188 'comment_id': comment.comment_id,
3180 3189 'comment_type': comment.comment_type,
3181 3190 'comment_text': comment.text,
3182 3191 'comment_status': comment.status_change,
3183 3192 'comment_f_path': comment.f_path,
3184 3193 'comment_lineno': comment.line_no,
3185 3194 'comment_author': comment.author,
3186 3195 'comment_created_on': comment.created_on
3187 3196 }
3188 3197 return data
3189 3198
3190 3199 def __json__(self):
3191 3200 data = dict()
3192 3201 data.update(self.get_api_data())
3193 3202 return data
3194 3203
3195 3204
3196 3205 class ChangesetStatus(Base, BaseModel):
3197 3206 __tablename__ = 'changeset_statuses'
3198 3207 __table_args__ = (
3199 3208 Index('cs_revision_idx', 'revision'),
3200 3209 Index('cs_version_idx', 'version'),
3201 3210 UniqueConstraint('repo_id', 'revision', 'version'),
3202 3211 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3203 3212 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3204 3213 )
3205 3214 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3206 3215 STATUS_APPROVED = 'approved'
3207 3216 STATUS_REJECTED = 'rejected'
3208 3217 STATUS_UNDER_REVIEW = 'under_review'
3209 3218
3210 3219 STATUSES = [
3211 3220 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3212 3221 (STATUS_APPROVED, _("Approved")),
3213 3222 (STATUS_REJECTED, _("Rejected")),
3214 3223 (STATUS_UNDER_REVIEW, _("Under Review")),
3215 3224 ]
3216 3225
3217 3226 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3218 3227 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3219 3228 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3220 3229 revision = Column('revision', String(40), nullable=False)
3221 3230 status = Column('status', String(128), nullable=False, default=DEFAULT)
3222 3231 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3223 3232 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3224 3233 version = Column('version', Integer(), nullable=False, default=0)
3225 3234 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3226 3235
3227 3236 author = relationship('User', lazy='joined')
3228 3237 repo = relationship('Repository')
3229 3238 comment = relationship('ChangesetComment', lazy='joined')
3230 3239 pull_request = relationship('PullRequest', lazy='joined')
3231 3240
3232 3241 def __unicode__(self):
3233 3242 return u"<%s('%s[v%s]:%s')>" % (
3234 3243 self.__class__.__name__,
3235 3244 self.status, self.version, self.author
3236 3245 )
3237 3246
3238 3247 @classmethod
3239 3248 def get_status_lbl(cls, value):
3240 3249 return dict(cls.STATUSES).get(value)
3241 3250
3242 3251 @property
3243 3252 def status_lbl(self):
3244 3253 return ChangesetStatus.get_status_lbl(self.status)
3245 3254
3246 3255 def get_api_data(self):
3247 3256 status = self
3248 3257 data = {
3249 3258 'status_id': status.changeset_status_id,
3250 3259 'status': status.status,
3251 3260 }
3252 3261 return data
3253 3262
3254 3263 def __json__(self):
3255 3264 data = dict()
3256 3265 data.update(self.get_api_data())
3257 3266 return data
3258 3267
3259 3268
3260 3269 class _PullRequestBase(BaseModel):
3261 3270 """
3262 3271 Common attributes of pull request and version entries.
3263 3272 """
3264 3273
3265 3274 # .status values
3266 3275 STATUS_NEW = u'new'
3267 3276 STATUS_OPEN = u'open'
3268 3277 STATUS_CLOSED = u'closed'
3269 3278
3270 3279 title = Column('title', Unicode(255), nullable=True)
3271 3280 description = Column(
3272 3281 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3273 3282 nullable=True)
3274 3283 # new/open/closed status of pull request (not approve/reject/etc)
3275 3284 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3276 3285 created_on = Column(
3277 3286 'created_on', DateTime(timezone=False), nullable=False,
3278 3287 default=datetime.datetime.now)
3279 3288 updated_on = Column(
3280 3289 'updated_on', DateTime(timezone=False), nullable=False,
3281 3290 default=datetime.datetime.now)
3282 3291
3283 3292 @declared_attr
3284 3293 def user_id(cls):
3285 3294 return Column(
3286 3295 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3287 3296 unique=None)
3288 3297
3289 3298 # 500 revisions max
3290 3299 _revisions = Column(
3291 3300 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3292 3301
3293 3302 @declared_attr
3294 3303 def source_repo_id(cls):
3295 3304 # TODO: dan: rename column to source_repo_id
3296 3305 return Column(
3297 3306 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3298 3307 nullable=False)
3299 3308
3300 3309 source_ref = Column('org_ref', Unicode(255), nullable=False)
3301 3310
3302 3311 @declared_attr
3303 3312 def target_repo_id(cls):
3304 3313 # TODO: dan: rename column to target_repo_id
3305 3314 return Column(
3306 3315 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3307 3316 nullable=False)
3308 3317
3309 3318 target_ref = Column('other_ref', Unicode(255), nullable=False)
3310 3319 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3311 3320
3312 3321 # TODO: dan: rename column to last_merge_source_rev
3313 3322 _last_merge_source_rev = Column(
3314 3323 'last_merge_org_rev', String(40), nullable=True)
3315 3324 # TODO: dan: rename column to last_merge_target_rev
3316 3325 _last_merge_target_rev = Column(
3317 3326 'last_merge_other_rev', String(40), nullable=True)
3318 3327 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3319 3328 merge_rev = Column('merge_rev', String(40), nullable=True)
3320 3329
3321 3330 reviewer_data = Column(
3322 3331 'reviewer_data_json', MutationObj.as_mutable(
3323 3332 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3324 3333
3325 3334 @property
3326 3335 def reviewer_data_json(self):
3327 3336 return json.dumps(self.reviewer_data)
3328 3337
3329 3338 @hybrid_property
3330 3339 def description_safe(self):
3331 3340 from rhodecode.lib import helpers as h
3332 3341 return h.escape(self.description)
3333 3342
3334 3343 @hybrid_property
3335 3344 def revisions(self):
3336 3345 return self._revisions.split(':') if self._revisions else []
3337 3346
3338 3347 @revisions.setter
3339 3348 def revisions(self, val):
3340 3349 self._revisions = ':'.join(val)
3341 3350
3342 3351 @hybrid_property
3343 3352 def last_merge_status(self):
3344 3353 return safe_int(self._last_merge_status)
3345 3354
3346 3355 @last_merge_status.setter
3347 3356 def last_merge_status(self, val):
3348 3357 self._last_merge_status = val
3349 3358
3350 3359 @declared_attr
3351 3360 def author(cls):
3352 3361 return relationship('User', lazy='joined')
3353 3362
3354 3363 @declared_attr
3355 3364 def source_repo(cls):
3356 3365 return relationship(
3357 3366 'Repository',
3358 3367 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3359 3368
3360 3369 @property
3361 3370 def source_ref_parts(self):
3362 3371 return self.unicode_to_reference(self.source_ref)
3363 3372
3364 3373 @declared_attr
3365 3374 def target_repo(cls):
3366 3375 return relationship(
3367 3376 'Repository',
3368 3377 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3369 3378
3370 3379 @property
3371 3380 def target_ref_parts(self):
3372 3381 return self.unicode_to_reference(self.target_ref)
3373 3382
3374 3383 @property
3375 3384 def shadow_merge_ref(self):
3376 3385 return self.unicode_to_reference(self._shadow_merge_ref)
3377 3386
3378 3387 @shadow_merge_ref.setter
3379 3388 def shadow_merge_ref(self, ref):
3380 3389 self._shadow_merge_ref = self.reference_to_unicode(ref)
3381 3390
3382 3391 def unicode_to_reference(self, raw):
3383 3392 """
3384 3393 Convert a unicode (or string) to a reference object.
3385 3394 If unicode evaluates to False it returns None.
3386 3395 """
3387 3396 if raw:
3388 3397 refs = raw.split(':')
3389 3398 return Reference(*refs)
3390 3399 else:
3391 3400 return None
3392 3401
3393 3402 def reference_to_unicode(self, ref):
3394 3403 """
3395 3404 Convert a reference object to unicode.
3396 3405 If reference is None it returns None.
3397 3406 """
3398 3407 if ref:
3399 3408 return u':'.join(ref)
3400 3409 else:
3401 3410 return None
3402 3411
3403 3412 def get_api_data(self, with_merge_state=True):
3404 3413 from rhodecode.model.pull_request import PullRequestModel
3405 3414
3406 3415 pull_request = self
3407 3416 if with_merge_state:
3408 3417 merge_status = PullRequestModel().merge_status(pull_request)
3409 3418 merge_state = {
3410 3419 'status': merge_status[0],
3411 3420 'message': safe_unicode(merge_status[1]),
3412 3421 }
3413 3422 else:
3414 3423 merge_state = {'status': 'not_available',
3415 3424 'message': 'not_available'}
3416 3425
3417 3426 merge_data = {
3418 3427 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3419 3428 'reference': (
3420 3429 pull_request.shadow_merge_ref._asdict()
3421 3430 if pull_request.shadow_merge_ref else None),
3422 3431 }
3423 3432
3424 3433 data = {
3425 3434 'pull_request_id': pull_request.pull_request_id,
3426 3435 'url': PullRequestModel().get_url(pull_request),
3427 3436 'title': pull_request.title,
3428 3437 'description': pull_request.description,
3429 3438 'status': pull_request.status,
3430 3439 'created_on': pull_request.created_on,
3431 3440 'updated_on': pull_request.updated_on,
3432 3441 'commit_ids': pull_request.revisions,
3433 3442 'review_status': pull_request.calculated_review_status(),
3434 3443 'mergeable': merge_state,
3435 3444 'source': {
3436 3445 'clone_url': pull_request.source_repo.clone_url(),
3437 3446 'repository': pull_request.source_repo.repo_name,
3438 3447 'reference': {
3439 3448 'name': pull_request.source_ref_parts.name,
3440 3449 'type': pull_request.source_ref_parts.type,
3441 3450 'commit_id': pull_request.source_ref_parts.commit_id,
3442 3451 },
3443 3452 },
3444 3453 'target': {
3445 3454 'clone_url': pull_request.target_repo.clone_url(),
3446 3455 'repository': pull_request.target_repo.repo_name,
3447 3456 'reference': {
3448 3457 'name': pull_request.target_ref_parts.name,
3449 3458 'type': pull_request.target_ref_parts.type,
3450 3459 'commit_id': pull_request.target_ref_parts.commit_id,
3451 3460 },
3452 3461 },
3453 3462 'merge': merge_data,
3454 3463 'author': pull_request.author.get_api_data(include_secrets=False,
3455 3464 details='basic'),
3456 3465 'reviewers': [
3457 3466 {
3458 3467 'user': reviewer.get_api_data(include_secrets=False,
3459 3468 details='basic'),
3460 3469 'reasons': reasons,
3461 3470 'review_status': st[0][1].status if st else 'not_reviewed',
3462 3471 }
3463 3472 for reviewer, reasons, mandatory, st in
3464 3473 pull_request.reviewers_statuses()
3465 3474 ]
3466 3475 }
3467 3476
3468 3477 return data
3469 3478
3470 3479
3471 3480 class PullRequest(Base, _PullRequestBase):
3472 3481 __tablename__ = 'pull_requests'
3473 3482 __table_args__ = (
3474 3483 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3475 3484 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3476 3485 )
3477 3486
3478 3487 pull_request_id = Column(
3479 3488 'pull_request_id', Integer(), nullable=False, primary_key=True)
3480 3489
3481 3490 def __repr__(self):
3482 3491 if self.pull_request_id:
3483 3492 return '<DB:PullRequest #%s>' % self.pull_request_id
3484 3493 else:
3485 3494 return '<DB:PullRequest at %#x>' % id(self)
3486 3495
3487 3496 reviewers = relationship('PullRequestReviewers',
3488 3497 cascade="all, delete, delete-orphan")
3489 3498 statuses = relationship('ChangesetStatus',
3490 3499 cascade="all, delete, delete-orphan")
3491 3500 comments = relationship('ChangesetComment',
3492 3501 cascade="all, delete, delete-orphan")
3493 3502 versions = relationship('PullRequestVersion',
3494 3503 cascade="all, delete, delete-orphan",
3495 3504 lazy='dynamic')
3496 3505
3497 3506 @classmethod
3498 3507 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3499 3508 internal_methods=None):
3500 3509
3501 3510 class PullRequestDisplay(object):
3502 3511 """
3503 3512 Special object wrapper for showing PullRequest data via Versions
3504 3513 It mimics PR object as close as possible. This is read only object
3505 3514 just for display
3506 3515 """
3507 3516
3508 3517 def __init__(self, attrs, internal=None):
3509 3518 self.attrs = attrs
3510 3519 # internal have priority over the given ones via attrs
3511 3520 self.internal = internal or ['versions']
3512 3521
3513 3522 def __getattr__(self, item):
3514 3523 if item in self.internal:
3515 3524 return getattr(self, item)
3516 3525 try:
3517 3526 return self.attrs[item]
3518 3527 except KeyError:
3519 3528 raise AttributeError(
3520 3529 '%s object has no attribute %s' % (self, item))
3521 3530
3522 3531 def __repr__(self):
3523 3532 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3524 3533
3525 3534 def versions(self):
3526 3535 return pull_request_obj.versions.order_by(
3527 3536 PullRequestVersion.pull_request_version_id).all()
3528 3537
3529 3538 def is_closed(self):
3530 3539 return pull_request_obj.is_closed()
3531 3540
3532 3541 @property
3533 3542 def pull_request_version_id(self):
3534 3543 return getattr(pull_request_obj, 'pull_request_version_id', None)
3535 3544
3536 3545 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3537 3546
3538 3547 attrs.author = StrictAttributeDict(
3539 3548 pull_request_obj.author.get_api_data())
3540 3549 if pull_request_obj.target_repo:
3541 3550 attrs.target_repo = StrictAttributeDict(
3542 3551 pull_request_obj.target_repo.get_api_data())
3543 3552 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3544 3553
3545 3554 if pull_request_obj.source_repo:
3546 3555 attrs.source_repo = StrictAttributeDict(
3547 3556 pull_request_obj.source_repo.get_api_data())
3548 3557 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3549 3558
3550 3559 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3551 3560 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3552 3561 attrs.revisions = pull_request_obj.revisions
3553 3562
3554 3563 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3555 3564 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3556 3565 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3557 3566
3558 3567 return PullRequestDisplay(attrs, internal=internal_methods)
3559 3568
3560 3569 def is_closed(self):
3561 3570 return self.status == self.STATUS_CLOSED
3562 3571
3563 3572 def __json__(self):
3564 3573 return {
3565 3574 'revisions': self.revisions,
3566 3575 }
3567 3576
3568 3577 def calculated_review_status(self):
3569 3578 from rhodecode.model.changeset_status import ChangesetStatusModel
3570 3579 return ChangesetStatusModel().calculated_review_status(self)
3571 3580
3572 3581 def reviewers_statuses(self):
3573 3582 from rhodecode.model.changeset_status import ChangesetStatusModel
3574 3583 return ChangesetStatusModel().reviewers_statuses(self)
3575 3584
3576 3585 @property
3577 3586 def workspace_id(self):
3578 3587 from rhodecode.model.pull_request import PullRequestModel
3579 3588 return PullRequestModel()._workspace_id(self)
3580 3589
3581 3590 def get_shadow_repo(self):
3582 3591 workspace_id = self.workspace_id
3583 3592 vcs_obj = self.target_repo.scm_instance()
3584 3593 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3585 3594 workspace_id)
3586 3595 return vcs_obj._get_shadow_instance(shadow_repository_path)
3587 3596
3588 3597
3589 3598 class PullRequestVersion(Base, _PullRequestBase):
3590 3599 __tablename__ = 'pull_request_versions'
3591 3600 __table_args__ = (
3592 3601 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3593 3602 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3594 3603 )
3595 3604
3596 3605 pull_request_version_id = Column(
3597 3606 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3598 3607 pull_request_id = Column(
3599 3608 'pull_request_id', Integer(),
3600 3609 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3601 3610 pull_request = relationship('PullRequest')
3602 3611
3603 3612 def __repr__(self):
3604 3613 if self.pull_request_version_id:
3605 3614 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3606 3615 else:
3607 3616 return '<DB:PullRequestVersion at %#x>' % id(self)
3608 3617
3609 3618 @property
3610 3619 def reviewers(self):
3611 3620 return self.pull_request.reviewers
3612 3621
3613 3622 @property
3614 3623 def versions(self):
3615 3624 return self.pull_request.versions
3616 3625
3617 3626 def is_closed(self):
3618 3627 # calculate from original
3619 3628 return self.pull_request.status == self.STATUS_CLOSED
3620 3629
3621 3630 def calculated_review_status(self):
3622 3631 return self.pull_request.calculated_review_status()
3623 3632
3624 3633 def reviewers_statuses(self):
3625 3634 return self.pull_request.reviewers_statuses()
3626 3635
3627 3636
3628 3637 class PullRequestReviewers(Base, BaseModel):
3629 3638 __tablename__ = 'pull_request_reviewers'
3630 3639 __table_args__ = (
3631 3640 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3632 3641 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3633 3642 )
3634 3643
3635 3644 @hybrid_property
3636 3645 def reasons(self):
3637 3646 if not self._reasons:
3638 3647 return []
3639 3648 return self._reasons
3640 3649
3641 3650 @reasons.setter
3642 3651 def reasons(self, val):
3643 3652 val = val or []
3644 3653 if any(not isinstance(x, basestring) for x in val):
3645 3654 raise Exception('invalid reasons type, must be list of strings')
3646 3655 self._reasons = val
3647 3656
3648 3657 pull_requests_reviewers_id = Column(
3649 3658 'pull_requests_reviewers_id', Integer(), nullable=False,
3650 3659 primary_key=True)
3651 3660 pull_request_id = Column(
3652 3661 "pull_request_id", Integer(),
3653 3662 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3654 3663 user_id = Column(
3655 3664 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3656 3665 _reasons = Column(
3657 3666 'reason', MutationList.as_mutable(
3658 3667 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3659 3668 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3660 3669 user = relationship('User')
3661 3670 pull_request = relationship('PullRequest')
3662 3671
3663 3672
3664 3673 class Notification(Base, BaseModel):
3665 3674 __tablename__ = 'notifications'
3666 3675 __table_args__ = (
3667 3676 Index('notification_type_idx', 'type'),
3668 3677 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3669 3678 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3670 3679 )
3671 3680
3672 3681 TYPE_CHANGESET_COMMENT = u'cs_comment'
3673 3682 TYPE_MESSAGE = u'message'
3674 3683 TYPE_MENTION = u'mention'
3675 3684 TYPE_REGISTRATION = u'registration'
3676 3685 TYPE_PULL_REQUEST = u'pull_request'
3677 3686 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3678 3687
3679 3688 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3680 3689 subject = Column('subject', Unicode(512), nullable=True)
3681 3690 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3682 3691 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3683 3692 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3684 3693 type_ = Column('type', Unicode(255))
3685 3694
3686 3695 created_by_user = relationship('User')
3687 3696 notifications_to_users = relationship('UserNotification', lazy='joined',
3688 3697 cascade="all, delete, delete-orphan")
3689 3698
3690 3699 @property
3691 3700 def recipients(self):
3692 3701 return [x.user for x in UserNotification.query()\
3693 3702 .filter(UserNotification.notification == self)\
3694 3703 .order_by(UserNotification.user_id.asc()).all()]
3695 3704
3696 3705 @classmethod
3697 3706 def create(cls, created_by, subject, body, recipients, type_=None):
3698 3707 if type_ is None:
3699 3708 type_ = Notification.TYPE_MESSAGE
3700 3709
3701 3710 notification = cls()
3702 3711 notification.created_by_user = created_by
3703 3712 notification.subject = subject
3704 3713 notification.body = body
3705 3714 notification.type_ = type_
3706 3715 notification.created_on = datetime.datetime.now()
3707 3716
3708 3717 for u in recipients:
3709 3718 assoc = UserNotification()
3710 3719 assoc.notification = notification
3711 3720
3712 3721 # if created_by is inside recipients mark his notification
3713 3722 # as read
3714 3723 if u.user_id == created_by.user_id:
3715 3724 assoc.read = True
3716 3725
3717 3726 u.notifications.append(assoc)
3718 3727 Session().add(notification)
3719 3728
3720 3729 return notification
3721 3730
3722 3731
3723 3732 class UserNotification(Base, BaseModel):
3724 3733 __tablename__ = 'user_to_notification'
3725 3734 __table_args__ = (
3726 3735 UniqueConstraint('user_id', 'notification_id'),
3727 3736 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3728 3737 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3729 3738 )
3730 3739 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3731 3740 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3732 3741 read = Column('read', Boolean, default=False)
3733 3742 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3734 3743
3735 3744 user = relationship('User', lazy="joined")
3736 3745 notification = relationship('Notification', lazy="joined",
3737 3746 order_by=lambda: Notification.created_on.desc(),)
3738 3747
3739 3748 def mark_as_read(self):
3740 3749 self.read = True
3741 3750 Session().add(self)
3742 3751
3743 3752
3744 3753 class Gist(Base, BaseModel):
3745 3754 __tablename__ = 'gists'
3746 3755 __table_args__ = (
3747 3756 Index('g_gist_access_id_idx', 'gist_access_id'),
3748 3757 Index('g_created_on_idx', 'created_on'),
3749 3758 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3750 3759 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3751 3760 )
3752 3761 GIST_PUBLIC = u'public'
3753 3762 GIST_PRIVATE = u'private'
3754 3763 DEFAULT_FILENAME = u'gistfile1.txt'
3755 3764
3756 3765 ACL_LEVEL_PUBLIC = u'acl_public'
3757 3766 ACL_LEVEL_PRIVATE = u'acl_private'
3758 3767
3759 3768 gist_id = Column('gist_id', Integer(), primary_key=True)
3760 3769 gist_access_id = Column('gist_access_id', Unicode(250))
3761 3770 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3762 3771 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3763 3772 gist_expires = Column('gist_expires', Float(53), nullable=False)
3764 3773 gist_type = Column('gist_type', Unicode(128), nullable=False)
3765 3774 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3766 3775 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3767 3776 acl_level = Column('acl_level', Unicode(128), nullable=True)
3768 3777
3769 3778 owner = relationship('User')
3770 3779
3771 3780 def __repr__(self):
3772 3781 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3773 3782
3774 3783 @hybrid_property
3775 3784 def description_safe(self):
3776 3785 from rhodecode.lib import helpers as h
3777 3786 return h.escape(self.gist_description)
3778 3787
3779 3788 @classmethod
3780 3789 def get_or_404(cls, id_):
3781 3790 from pyramid.httpexceptions import HTTPNotFound
3782 3791
3783 3792 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3784 3793 if not res:
3785 3794 raise HTTPNotFound()
3786 3795 return res
3787 3796
3788 3797 @classmethod
3789 3798 def get_by_access_id(cls, gist_access_id):
3790 3799 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3791 3800
3792 3801 def gist_url(self):
3793 3802 from rhodecode.model.gist import GistModel
3794 3803 return GistModel().get_url(self)
3795 3804
3796 3805 @classmethod
3797 3806 def base_path(cls):
3798 3807 """
3799 3808 Returns base path when all gists are stored
3800 3809
3801 3810 :param cls:
3802 3811 """
3803 3812 from rhodecode.model.gist import GIST_STORE_LOC
3804 3813 q = Session().query(RhodeCodeUi)\
3805 3814 .filter(RhodeCodeUi.ui_key == URL_SEP)
3806 3815 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3807 3816 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3808 3817
3809 3818 def get_api_data(self):
3810 3819 """
3811 3820 Common function for generating gist related data for API
3812 3821 """
3813 3822 gist = self
3814 3823 data = {
3815 3824 'gist_id': gist.gist_id,
3816 3825 'type': gist.gist_type,
3817 3826 'access_id': gist.gist_access_id,
3818 3827 'description': gist.gist_description,
3819 3828 'url': gist.gist_url(),
3820 3829 'expires': gist.gist_expires,
3821 3830 'created_on': gist.created_on,
3822 3831 'modified_at': gist.modified_at,
3823 3832 'content': None,
3824 3833 'acl_level': gist.acl_level,
3825 3834 }
3826 3835 return data
3827 3836
3828 3837 def __json__(self):
3829 3838 data = dict(
3830 3839 )
3831 3840 data.update(self.get_api_data())
3832 3841 return data
3833 3842 # SCM functions
3834 3843
3835 3844 def scm_instance(self, **kwargs):
3836 3845 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3837 3846 return get_vcs_instance(
3838 3847 repo_path=safe_str(full_repo_path), create=False)
3839 3848
3840 3849
3841 3850 class ExternalIdentity(Base, BaseModel):
3842 3851 __tablename__ = 'external_identities'
3843 3852 __table_args__ = (
3844 3853 Index('local_user_id_idx', 'local_user_id'),
3845 3854 Index('external_id_idx', 'external_id'),
3846 3855 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3847 3856 'mysql_charset': 'utf8'})
3848 3857
3849 3858 external_id = Column('external_id', Unicode(255), default=u'',
3850 3859 primary_key=True)
3851 3860 external_username = Column('external_username', Unicode(1024), default=u'')
3852 3861 local_user_id = Column('local_user_id', Integer(),
3853 3862 ForeignKey('users.user_id'), primary_key=True)
3854 3863 provider_name = Column('provider_name', Unicode(255), default=u'',
3855 3864 primary_key=True)
3856 3865 access_token = Column('access_token', String(1024), default=u'')
3857 3866 alt_token = Column('alt_token', String(1024), default=u'')
3858 3867 token_secret = Column('token_secret', String(1024), default=u'')
3859 3868
3860 3869 @classmethod
3861 3870 def by_external_id_and_provider(cls, external_id, provider_name,
3862 3871 local_user_id=None):
3863 3872 """
3864 3873 Returns ExternalIdentity instance based on search params
3865 3874
3866 3875 :param external_id:
3867 3876 :param provider_name:
3868 3877 :return: ExternalIdentity
3869 3878 """
3870 3879 query = cls.query()
3871 3880 query = query.filter(cls.external_id == external_id)
3872 3881 query = query.filter(cls.provider_name == provider_name)
3873 3882 if local_user_id:
3874 3883 query = query.filter(cls.local_user_id == local_user_id)
3875 3884 return query.first()
3876 3885
3877 3886 @classmethod
3878 3887 def user_by_external_id_and_provider(cls, external_id, provider_name):
3879 3888 """
3880 3889 Returns User instance based on search params
3881 3890
3882 3891 :param external_id:
3883 3892 :param provider_name:
3884 3893 :return: User
3885 3894 """
3886 3895 query = User.query()
3887 3896 query = query.filter(cls.external_id == external_id)
3888 3897 query = query.filter(cls.provider_name == provider_name)
3889 3898 query = query.filter(User.user_id == cls.local_user_id)
3890 3899 return query.first()
3891 3900
3892 3901 @classmethod
3893 3902 def by_local_user_id(cls, local_user_id):
3894 3903 """
3895 3904 Returns all tokens for user
3896 3905
3897 3906 :param local_user_id:
3898 3907 :return: ExternalIdentity
3899 3908 """
3900 3909 query = cls.query()
3901 3910 query = query.filter(cls.local_user_id == local_user_id)
3902 3911 return query
3903 3912
3904 3913
3905 3914 class Integration(Base, BaseModel):
3906 3915 __tablename__ = 'integrations'
3907 3916 __table_args__ = (
3908 3917 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3909 3918 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3910 3919 )
3911 3920
3912 3921 integration_id = Column('integration_id', Integer(), primary_key=True)
3913 3922 integration_type = Column('integration_type', String(255))
3914 3923 enabled = Column('enabled', Boolean(), nullable=False)
3915 3924 name = Column('name', String(255), nullable=False)
3916 3925 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3917 3926 default=False)
3918 3927
3919 3928 settings = Column(
3920 3929 'settings_json', MutationObj.as_mutable(
3921 3930 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3922 3931 repo_id = Column(
3923 3932 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3924 3933 nullable=True, unique=None, default=None)
3925 3934 repo = relationship('Repository', lazy='joined')
3926 3935
3927 3936 repo_group_id = Column(
3928 3937 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3929 3938 nullable=True, unique=None, default=None)
3930 3939 repo_group = relationship('RepoGroup', lazy='joined')
3931 3940
3932 3941 @property
3933 3942 def scope(self):
3934 3943 if self.repo:
3935 3944 return repr(self.repo)
3936 3945 if self.repo_group:
3937 3946 if self.child_repos_only:
3938 3947 return repr(self.repo_group) + ' (child repos only)'
3939 3948 else:
3940 3949 return repr(self.repo_group) + ' (recursive)'
3941 3950 if self.child_repos_only:
3942 3951 return 'root_repos'
3943 3952 return 'global'
3944 3953
3945 3954 def __repr__(self):
3946 3955 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3947 3956
3948 3957
3949 3958 class RepoReviewRuleUser(Base, BaseModel):
3950 3959 __tablename__ = 'repo_review_rules_users'
3951 3960 __table_args__ = (
3952 3961 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3953 3962 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3954 3963 )
3955 3964 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
3956 3965 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3957 3966 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
3958 3967 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3959 3968 user = relationship('User')
3960 3969
3961 3970 def rule_data(self):
3962 3971 return {
3963 3972 'mandatory': self.mandatory
3964 3973 }
3965 3974
3966 3975
3967 3976 class RepoReviewRuleUserGroup(Base, BaseModel):
3968 3977 __tablename__ = 'repo_review_rules_users_groups'
3969 3978 __table_args__ = (
3970 3979 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3971 3980 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3972 3981 )
3973 3982 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
3974 3983 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3975 3984 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
3976 3985 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3977 3986 users_group = relationship('UserGroup')
3978 3987
3979 3988 def rule_data(self):
3980 3989 return {
3981 3990 'mandatory': self.mandatory
3982 3991 }
3983 3992
3984 3993
3985 3994 class RepoReviewRule(Base, BaseModel):
3986 3995 __tablename__ = 'repo_review_rules'
3987 3996 __table_args__ = (
3988 3997 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3989 3998 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3990 3999 )
3991 4000
3992 4001 repo_review_rule_id = Column(
3993 4002 'repo_review_rule_id', Integer(), primary_key=True)
3994 4003 repo_id = Column(
3995 4004 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3996 4005 repo = relationship('Repository', backref='review_rules')
3997 4006
3998 4007 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
3999 4008 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4000 4009
4001 4010 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4002 4011 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4003 4012 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4004 4013 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4005 4014
4006 4015 rule_users = relationship('RepoReviewRuleUser')
4007 4016 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4008 4017
4009 4018 @hybrid_property
4010 4019 def branch_pattern(self):
4011 4020 return self._branch_pattern or '*'
4012 4021
4013 4022 def _validate_glob(self, value):
4014 4023 re.compile('^' + glob2re(value) + '$')
4015 4024
4016 4025 @branch_pattern.setter
4017 4026 def branch_pattern(self, value):
4018 4027 self._validate_glob(value)
4019 4028 self._branch_pattern = value or '*'
4020 4029
4021 4030 @hybrid_property
4022 4031 def file_pattern(self):
4023 4032 return self._file_pattern or '*'
4024 4033
4025 4034 @file_pattern.setter
4026 4035 def file_pattern(self, value):
4027 4036 self._validate_glob(value)
4028 4037 self._file_pattern = value or '*'
4029 4038
4030 4039 def matches(self, branch, files_changed):
4031 4040 """
4032 4041 Check if this review rule matches a branch/files in a pull request
4033 4042
4034 4043 :param branch: branch name for the commit
4035 4044 :param files_changed: list of file paths changed in the pull request
4036 4045 """
4037 4046
4038 4047 branch = branch or ''
4039 4048 files_changed = files_changed or []
4040 4049
4041 4050 branch_matches = True
4042 4051 if branch:
4043 4052 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
4044 4053 branch_matches = bool(branch_regex.search(branch))
4045 4054
4046 4055 files_matches = True
4047 4056 if self.file_pattern != '*':
4048 4057 files_matches = False
4049 4058 file_regex = re.compile(glob2re(self.file_pattern))
4050 4059 for filename in files_changed:
4051 4060 if file_regex.search(filename):
4052 4061 files_matches = True
4053 4062 break
4054 4063
4055 4064 return branch_matches and files_matches
4056 4065
4057 4066 @property
4058 4067 def review_users(self):
4059 4068 """ Returns the users which this rule applies to """
4060 4069
4061 4070 users = collections.OrderedDict()
4062 4071
4063 4072 for rule_user in self.rule_users:
4064 4073 if rule_user.user.active:
4065 4074 if rule_user.user not in users:
4066 4075 users[rule_user.user.username] = {
4067 4076 'user': rule_user.user,
4068 4077 'source': 'user',
4069 4078 'source_data': {},
4070 4079 'data': rule_user.rule_data()
4071 4080 }
4072 4081
4073 4082 for rule_user_group in self.rule_user_groups:
4074 4083 source_data = {
4075 4084 'name': rule_user_group.users_group.users_group_name,
4076 4085 'members': len(rule_user_group.users_group.members)
4077 4086 }
4078 4087 for member in rule_user_group.users_group.members:
4079 4088 if member.user.active:
4080 4089 users[member.user.username] = {
4081 4090 'user': member.user,
4082 4091 'source': 'user_group',
4083 4092 'source_data': source_data,
4084 4093 'data': rule_user_group.rule_data()
4085 4094 }
4086 4095
4087 4096 return users
4088 4097
4089 4098 def __repr__(self):
4090 4099 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4091 4100 self.repo_review_rule_id, self.repo)
4092 4101
4093 4102
4094 4103 class DbMigrateVersion(Base, BaseModel):
4095 4104 __tablename__ = 'db_migrate_version'
4096 4105 __table_args__ = (
4097 4106 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4098 4107 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4099 4108 )
4100 4109 repository_id = Column('repository_id', String(250), primary_key=True)
4101 4110 repository_path = Column('repository_path', Text)
4102 4111 version = Column('version', Integer)
4103 4112
4104 4113
4105 4114 class DbSession(Base, BaseModel):
4106 4115 __tablename__ = 'db_session'
4107 4116 __table_args__ = (
4108 4117 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4109 4118 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4110 4119 )
4111 4120
4112 4121 def __repr__(self):
4113 4122 return '<DB:DbSession({})>'.format(self.id)
4114 4123
4115 4124 id = Column('id', Integer())
4116 4125 namespace = Column('namespace', String(255), primary_key=True)
4117 4126 accessed = Column('accessed', DateTime, nullable=False)
4118 4127 created = Column('created', DateTime, nullable=False)
4119 4128 data = Column('data', PickleType, nullable=False)
@@ -1,218 +1,220 b''
1 1
2 2 /******************************************************************************
3 3 * *
4 4 * DO NOT CHANGE THIS FILE MANUALLY *
5 5 * *
6 6 * *
7 7 * This file is automatically generated when the app starts up with *
8 8 * generate_js_files = true *
9 9 * *
10 10 * To add a route here pass jsroute=True to the route definition in the app *
11 11 * *
12 12 ******************************************************************************/
13 13 function registerRCRoutes() {
14 14 // routes registration
15 15 pyroutes.register('new_repo', '/_admin/create_repository', []);
16 16 pyroutes.register('edit_user', '/_admin/users/%(user_id)s/edit', ['user_id']);
17 pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']);
18 17 pyroutes.register('favicon', '/favicon.ico', []);
19 18 pyroutes.register('robots', '/robots.txt', []);
20 19 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
21 20 pyroutes.register('global_integrations_new', '/_admin/integrations/new', []);
22 21 pyroutes.register('global_integrations_home', '/_admin/integrations', []);
23 22 pyroutes.register('global_integrations_list', '/_admin/integrations/%(integration)s', ['integration']);
24 23 pyroutes.register('global_integrations_create', '/_admin/integrations/%(integration)s/new', ['integration']);
25 24 pyroutes.register('global_integrations_edit', '/_admin/integrations/%(integration)s/%(integration_id)s', ['integration', 'integration_id']);
26 25 pyroutes.register('repo_group_integrations_home', '/%(repo_group_name)s/settings/integrations', ['repo_group_name']);
27 26 pyroutes.register('repo_group_integrations_list', '/%(repo_group_name)s/settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
28 27 pyroutes.register('repo_group_integrations_new', '/%(repo_group_name)s/settings/integrations/new', ['repo_group_name']);
29 28 pyroutes.register('repo_group_integrations_create', '/%(repo_group_name)s/settings/integrations/%(integration)s/new', ['repo_group_name', 'integration']);
30 29 pyroutes.register('repo_group_integrations_edit', '/%(repo_group_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_group_name', 'integration', 'integration_id']);
31 30 pyroutes.register('repo_integrations_home', '/%(repo_name)s/settings/integrations', ['repo_name']);
32 31 pyroutes.register('repo_integrations_list', '/%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
33 32 pyroutes.register('repo_integrations_new', '/%(repo_name)s/settings/integrations/new', ['repo_name']);
34 33 pyroutes.register('repo_integrations_create', '/%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
35 34 pyroutes.register('repo_integrations_edit', '/%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
36 35 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
37 36 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
38 37 pyroutes.register('ops_redirect_test', '/_admin/ops/redirect', []);
39 38 pyroutes.register('admin_home', '/_admin', []);
40 39 pyroutes.register('admin_audit_logs', '/_admin/audit_logs', []);
41 40 pyroutes.register('pull_requests_global_0', '/_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
42 41 pyroutes.register('pull_requests_global_1', '/_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
43 42 pyroutes.register('pull_requests_global', '/_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
44 43 pyroutes.register('admin_settings_open_source', '/_admin/settings/open_source', []);
45 44 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '/_admin/settings/vcs/svn_generate_cfg', []);
46 45 pyroutes.register('admin_settings_system', '/_admin/settings/system', []);
47 46 pyroutes.register('admin_settings_system_update', '/_admin/settings/system/updates', []);
48 47 pyroutes.register('admin_settings_sessions', '/_admin/settings/sessions', []);
49 48 pyroutes.register('admin_settings_sessions_cleanup', '/_admin/settings/sessions/cleanup', []);
50 49 pyroutes.register('admin_settings_process_management', '/_admin/settings/process_management', []);
51 50 pyroutes.register('admin_settings_process_management_signal', '/_admin/settings/process_management/signal', []);
52 51 pyroutes.register('admin_permissions_application', '/_admin/permissions/application', []);
53 52 pyroutes.register('admin_permissions_application_update', '/_admin/permissions/application/update', []);
54 53 pyroutes.register('admin_permissions_global', '/_admin/permissions/global', []);
55 54 pyroutes.register('admin_permissions_global_update', '/_admin/permissions/global/update', []);
56 55 pyroutes.register('admin_permissions_object', '/_admin/permissions/object', []);
57 56 pyroutes.register('admin_permissions_object_update', '/_admin/permissions/object/update', []);
58 57 pyroutes.register('admin_permissions_ips', '/_admin/permissions/ips', []);
59 58 pyroutes.register('admin_permissions_overview', '/_admin/permissions/overview', []);
60 59 pyroutes.register('admin_permissions_auth_token_access', '/_admin/permissions/auth_token_access', []);
61 60 pyroutes.register('users', '/_admin/users', []);
62 61 pyroutes.register('users_data', '/_admin/users_data', []);
63 62 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
64 63 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
65 64 pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
66 65 pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']);
67 66 pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']);
68 67 pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']);
69 68 pyroutes.register('edit_user_ips', '/_admin/users/%(user_id)s/edit/ips', ['user_id']);
70 69 pyroutes.register('edit_user_ips_add', '/_admin/users/%(user_id)s/edit/ips/new', ['user_id']);
71 70 pyroutes.register('edit_user_ips_delete', '/_admin/users/%(user_id)s/edit/ips/delete', ['user_id']);
72 71 pyroutes.register('edit_user_groups_management', '/_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
73 72 pyroutes.register('edit_user_groups_management_updates', '/_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
74 73 pyroutes.register('edit_user_audit_logs', '/_admin/users/%(user_id)s/edit/audit', ['user_id']);
74 pyroutes.register('user_groups', '/_admin/user_groups', []);
75 pyroutes.register('user_groups_data', '/_admin/user_groups_data', []);
76 pyroutes.register('user_group_members_data', '/_admin/user_groups/%(user_group_id)s/members', ['user_group_id']);
75 77 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
76 78 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
77 79 pyroutes.register('channelstream_proxy', '/_channelstream', []);
78 80 pyroutes.register('login', '/_admin/login', []);
79 81 pyroutes.register('logout', '/_admin/logout', []);
80 82 pyroutes.register('register', '/_admin/register', []);
81 83 pyroutes.register('reset_password', '/_admin/password_reset', []);
82 84 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
83 85 pyroutes.register('home', '/', []);
84 86 pyroutes.register('user_autocomplete_data', '/_users', []);
85 87 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
86 88 pyroutes.register('repo_list_data', '/_repos', []);
87 89 pyroutes.register('goto_switcher_data', '/_goto_data', []);
88 90 pyroutes.register('journal', '/_admin/journal', []);
89 91 pyroutes.register('journal_rss', '/_admin/journal/rss', []);
90 92 pyroutes.register('journal_atom', '/_admin/journal/atom', []);
91 93 pyroutes.register('journal_public', '/_admin/public_journal', []);
92 94 pyroutes.register('journal_public_atom', '/_admin/public_journal/atom', []);
93 95 pyroutes.register('journal_public_atom_old', '/_admin/public_journal_atom', []);
94 96 pyroutes.register('journal_public_rss', '/_admin/public_journal/rss', []);
95 97 pyroutes.register('journal_public_rss_old', '/_admin/public_journal_rss', []);
96 98 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
97 99 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
98 100 pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
99 101 pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']);
100 102 pyroutes.register('repo_commit_children', '/%(repo_name)s/changeset_children/%(commit_id)s', ['repo_name', 'commit_id']);
101 103 pyroutes.register('repo_commit_parents', '/%(repo_name)s/changeset_parents/%(commit_id)s', ['repo_name', 'commit_id']);
102 104 pyroutes.register('repo_commit_raw', '/%(repo_name)s/changeset-diff/%(commit_id)s', ['repo_name', 'commit_id']);
103 105 pyroutes.register('repo_commit_patch', '/%(repo_name)s/changeset-patch/%(commit_id)s', ['repo_name', 'commit_id']);
104 106 pyroutes.register('repo_commit_download', '/%(repo_name)s/changeset-download/%(commit_id)s', ['repo_name', 'commit_id']);
105 107 pyroutes.register('repo_commit_data', '/%(repo_name)s/changeset-data/%(commit_id)s', ['repo_name', 'commit_id']);
106 108 pyroutes.register('repo_commit_comment_create', '/%(repo_name)s/changeset/%(commit_id)s/comment/create', ['repo_name', 'commit_id']);
107 109 pyroutes.register('repo_commit_comment_preview', '/%(repo_name)s/changeset/%(commit_id)s/comment/preview', ['repo_name', 'commit_id']);
108 110 pyroutes.register('repo_commit_comment_delete', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/delete', ['repo_name', 'commit_id', 'comment_id']);
109 111 pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
110 112 pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
111 113 pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
112 114 pyroutes.register('repo_files_diff_2way_redirect', '/%(repo_name)s/diff-2way/%(f_path)s', ['repo_name', 'f_path']);
113 115 pyroutes.register('repo_files', '/%(repo_name)s/files/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
114 116 pyroutes.register('repo_files:default_path', '/%(repo_name)s/files/%(commit_id)s/', ['repo_name', 'commit_id']);
115 117 pyroutes.register('repo_files:default_commit', '/%(repo_name)s/files', ['repo_name']);
116 118 pyroutes.register('repo_files:rendered', '/%(repo_name)s/render/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
117 119 pyroutes.register('repo_files:annotated', '/%(repo_name)s/annotate/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
118 120 pyroutes.register('repo_files:annotated_previous', '/%(repo_name)s/annotate-previous/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
119 121 pyroutes.register('repo_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
120 122 pyroutes.register('repo_nodetree_full:default_path', '/%(repo_name)s/nodetree_full/%(commit_id)s/', ['repo_name', 'commit_id']);
121 123 pyroutes.register('repo_files_nodelist', '/%(repo_name)s/nodelist/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
122 124 pyroutes.register('repo_file_raw', '/%(repo_name)s/raw/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
123 125 pyroutes.register('repo_file_download', '/%(repo_name)s/download/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
124 126 pyroutes.register('repo_file_download:legacy', '/%(repo_name)s/rawfile/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
125 127 pyroutes.register('repo_file_history', '/%(repo_name)s/history/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
126 128 pyroutes.register('repo_file_authors', '/%(repo_name)s/authors/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
127 129 pyroutes.register('repo_files_remove_file', '/%(repo_name)s/remove_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
128 130 pyroutes.register('repo_files_delete_file', '/%(repo_name)s/delete_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
129 131 pyroutes.register('repo_files_edit_file', '/%(repo_name)s/edit_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
130 132 pyroutes.register('repo_files_update_file', '/%(repo_name)s/update_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
131 133 pyroutes.register('repo_files_add_file', '/%(repo_name)s/add_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
132 134 pyroutes.register('repo_files_create_file', '/%(repo_name)s/create_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
133 135 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
134 136 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
135 137 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
136 138 pyroutes.register('repo_changelog', '/%(repo_name)s/changelog', ['repo_name']);
137 139 pyroutes.register('repo_changelog_file', '/%(repo_name)s/changelog/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
138 140 pyroutes.register('repo_changelog_elements', '/%(repo_name)s/changelog_elements', ['repo_name']);
139 141 pyroutes.register('repo_compare_select', '/%(repo_name)s/compare', ['repo_name']);
140 142 pyroutes.register('repo_compare', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']);
141 143 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
142 144 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
143 145 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
144 146 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
145 147 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
146 148 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
147 149 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
148 150 pyroutes.register('pullrequest_repo_destinations', '/%(repo_name)s/pull-request/repo-destinations', ['repo_name']);
149 151 pyroutes.register('pullrequest_new', '/%(repo_name)s/pull-request/new', ['repo_name']);
150 152 pyroutes.register('pullrequest_create', '/%(repo_name)s/pull-request/create', ['repo_name']);
151 153 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s/update', ['repo_name', 'pull_request_id']);
152 154 pyroutes.register('pullrequest_merge', '/%(repo_name)s/pull-request/%(pull_request_id)s/merge', ['repo_name', 'pull_request_id']);
153 155 pyroutes.register('pullrequest_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/delete', ['repo_name', 'pull_request_id']);
154 156 pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
155 157 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/delete', ['repo_name', 'pull_request_id', 'comment_id']);
156 158 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
157 159 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
158 160 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
159 161 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
160 162 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
161 163 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
162 164 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
163 165 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
164 166 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
165 167 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
166 168 pyroutes.register('repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
167 169 pyroutes.register('repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
168 170 pyroutes.register('strip', '/%(repo_name)s/settings/strip', ['repo_name']);
169 171 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
170 172 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
171 173 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed/rss', ['repo_name']);
172 174 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed/atom', ['repo_name']);
173 175 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
174 176 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
175 177 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
176 178 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
177 179 pyroutes.register('search', '/_admin/search', []);
178 180 pyroutes.register('search_repo', '/%(repo_name)s/search', ['repo_name']);
179 181 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
180 182 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
181 183 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
182 184 pyroutes.register('my_account_update', '/_admin/my_account/update', []);
183 185 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
184 186 pyroutes.register('my_account_password_update', '/_admin/my_account/password/update', []);
185 187 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
186 188 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
187 189 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
188 190 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
189 191 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
190 192 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
191 193 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
192 194 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
193 195 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
194 196 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
195 197 pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []);
196 198 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
197 199 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
198 200 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
199 201 pyroutes.register('notifications_mark_all_read', '/_admin/notifications/mark_all_read', []);
200 202 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
201 203 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
202 204 pyroutes.register('notifications_delete', '/_admin/notifications/%(notification_id)s/delete', ['notification_id']);
203 205 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
204 206 pyroutes.register('gists_show', '/_admin/gists', []);
205 207 pyroutes.register('gists_new', '/_admin/gists/new', []);
206 208 pyroutes.register('gists_create', '/_admin/gists/create', []);
207 209 pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']);
208 210 pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']);
209 211 pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']);
210 212 pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']);
211 213 pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']);
212 214 pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/%(revision)s', ['gist_id', 'revision']);
213 215 pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
214 216 pyroutes.register('gist_show_formatted_path', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s/%(f_path)s', ['gist_id', 'revision', 'format', 'f_path']);
215 217 pyroutes.register('debug_style_home', '/_admin/debug_style', []);
216 218 pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
217 219 pyroutes.register('apiv2', '/_admin/api', []);
218 220 }
@@ -1,186 +1,186 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%namespace name="base" file="/base/base.mako"/>
3 3
4 4 <div class="panel panel-default">
5 5 <div class="panel-heading">
6 6 <h3 class="panel-title">${_('User Group: %s') % c.user_group.users_group_name}</h3>
7 7 </div>
8 8 <div class="panel-body">
9 9 ${h.secure_form(h.url('update_users_group', user_group_id=c.user_group.users_group_id),method='put', id='edit_users_group')}
10 10 <div class="form">
11 11 <!-- fields -->
12 12 <div class="fields">
13 13 <div class="field">
14 14 <div class="label">
15 15 <label for="users_group_name">${_('Group name')}:</label>
16 16 </div>
17 17 <div class="input">
18 18 ${h.text('users_group_name',class_='medium')}
19 19 </div>
20 20 </div>
21 21
22 22 <div class="field badged-field">
23 23 <div class="label">
24 24 <label for="user">${_('Owner')}:</label>
25 25 </div>
26 26 <div class="input">
27 27 <div class="badge-input-container">
28 28 <div class="user-badge">
29 29 ${base.gravatar_with_user(c.user_group.user.email, show_disabled=not c.user_group.user.active)}
30 30 </div>
31 31 <div class="badge-input-wrap">
32 32 ${h.text('user', class_="medium", autocomplete="off")}
33 33 </div>
34 34 </div>
35 35 <form:error name="user"/>
36 36 <p class="help-block">${_('Change owner of this user group.')}</p>
37 37 </div>
38 38 </div>
39 39
40 40 <div class="field">
41 41 <div class="label label-textarea">
42 42 <label for="user_group_description">${_('Description')}:</label>
43 43 </div>
44 44 <div class="textarea textarea-small editor">
45 45 ${h.textarea('user_group_description',cols=23,rows=5,class_="medium")}
46 46 <span class="help-block">${_('Short, optional description for this user group.')}</span>
47 47 </div>
48 48 </div>
49 49 <div class="field">
50 50 <div class="label label-checkbox">
51 51 <label for="users_group_active">${_('Active')}:</label>
52 52 </div>
53 53 <div class="checkboxes">
54 54 ${h.checkbox('users_group_active',value=True)}
55 55 </div>
56 56 </div>
57 57
58 58 <div class="field">
59 59 <div class="label label-checkbox">
60 60 <label for="users_group_active">${_('Add members')}:</label>
61 61 </div>
62 62 <div class="input">
63 63 ${h.text('user_group_add_members', placeholder="user/usergroup", class_="medium")}
64 64 </div>
65 65 </div>
66 66
67 67 <input type="hidden" name="__start__" value="user_group_members:sequence"/>
68 68 <table id="group_members_placeholder" class="rctable group_members">
69 69 <tr>
70 70 <th>${_('Username')}</th>
71 71 <th>${_('Action')}</th>
72 72 </tr>
73 73
74 74 % if c.group_members_obj:
75 75 % for user in c.group_members_obj:
76 76 <tr>
77 77 <td id="member_user_${user.user_id}" class="td-author">
78 78 <div class="group_member">
79 79 ${base.gravatar(user.email, 16)}
80 80 <span class="username user">${h.link_to(h.person(user), h.url( 'edit_user',user_id=user.user_id))}</span>
81 81 <input type="hidden" name="__start__" value="member:mapping">
82 82 <input type="hidden" name="member_user_id" value="${user.user_id}">
83 83 <input type="hidden" name="type" value="existing" id="member_${user.user_id}">
84 84 <input type="hidden" name="__end__" value="member:mapping">
85 85 </div>
86 86 </td>
87 87 <td class="">
88 88 <div class="usergroup_member_remove action_button" onclick="removeUserGroupMember(${user.user_id}, true)" style="visibility: visible;">
89 89 <i class="icon-remove-sign"></i>
90 90 </div>
91 91 </td>
92 92 </tr>
93 93 % endfor
94 94
95 95 % else:
96 96 <tr><td colspan="2">${_('No members yet')}</td></tr>
97 97 % endif
98 98 </table>
99 99 <input type="hidden" name="__end__" value="user_group_members:sequence"/>
100 100
101 101 <div class="buttons">
102 102 ${h.submit('Save',_('Save'),class_="btn")}
103 103 </div>
104 104 </div>
105 105 </div>
106 106 ${h.end_form()}
107 107 </div>
108 108 </div>
109 109 <script>
110 110 $(document).ready(function(){
111 111 $("#group_parent_id").select2({
112 112 'containerCssClass': "drop-menu",
113 113 'dropdownCssClass': "drop-menu-dropdown",
114 114 'dropdownAutoWidth': true
115 115 });
116 116
117 117 removeUserGroupMember = function(userId){
118 118 $('#member_'+userId).val('remove');
119 119 $('#member_user_'+userId).addClass('to-delete');
120 120 };
121 121
122 122 $('#user_group_add_members').autocomplete({
123 123 serviceUrl: pyroutes.url('user_autocomplete_data'),
124 124 minChars:2,
125 125 maxHeight:400,
126 126 width:300,
127 127 deferRequestBy: 300, //miliseconds
128 128 showNoSuggestionNotice: true,
129 129 params: { user_groups:true },
130 130 formatResult: autocompleteFormatResult,
131 131 lookupFilter: autocompleteFilterResult,
132 132 onSelect: function(element, suggestion){
133 133
134 134 function addMember(user, fromUserGroup) {
135 135 var gravatar = user.icon_link;
136 136 var username = user.value_display;
137 137 var userLink = pyroutes.url('edit_user', {"user_id": user.id});
138 138 var uid = user.id;
139 139
140 140 if (fromUserGroup) {
141 141 username = username +" "+ _gettext('(from usergroup {0})'.format(fromUserGroup))
142 142 }
143 143
144 144 var elem = $(
145 145 ('<tr>'+
146 146 '<td id="member_user_{6}" class="td-author td-author-new-entry">'+
147 147 '<div class="group_member">'+
148 148 '<img class="gravatar" src="{0}" height="16" width="16">'+
149 149 '<span class="username user"><a href="{1}">{2}</a></span>'+
150 150 '<input type="hidden" name="__start__" value="member:mapping">'+
151 151 '<input type="hidden" name="member_user_id" value="{3}">'+
152 152 '<input type="hidden" name="type" value="new" id="member_{4}">'+
153 153 '<input type="hidden" name="__end__" value="member:mapping">'+
154 154 '</div>'+
155 155 '</td>'+
156 156 '<td class="td-author-new-entry">'+
157 157 '<div class="usergroup_member_remove action_button" onclick="removeUserGroupMember({5}, true)" style="visibility: visible;">'+
158 158 '<i class="icon-remove-sign"></i>'+
159 159 '</div>'+
160 160 '</td>'+
161 161 '</tr>').format(gravatar, userLink, username,
162 162 uid, uid, uid, uid)
163 163 );
164 164 $('#group_members_placeholder').append(elem)
165 165 }
166 166
167 167 if (suggestion.value_type == 'user_group') {
168 168 $.getJSON(
169 pyroutes.url('edit_user_group_members',
169 pyroutes.url('user_group_members_data',
170 170 {'user_group_id': suggestion.id}),
171 171 function(data) {
172 172 $.each(data.members, function(idx, user) {
173 173 addMember(user, suggestion.value)
174 174 });
175 175 }
176 176 );
177 177 } else if (suggestion.value_type == 'user') {
178 178 addMember(suggestion, null);
179 179 }
180 180 }
181 181 });
182 182
183 183
184 184 UsersAutoComplete('user', '${c.rhodecode_user.user_id}');
185 185 })
186 186 </script>
@@ -1,100 +1,108 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${_('User groups administration')}
6 6 %if c.rhodecode_name:
7 7 &middot; ${h.branding(c.rhodecode_name)}
8 8 %endif
9 9 </%def>
10 10
11 11 <%def name="breadcrumbs_links()">
12 12 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
13 13 ${h.link_to(_('Admin'),h.route_path('admin_home'))} &raquo; <span id="user_group_count">0</span> ${_('user groups')}
14 14 </%def>
15 15
16 16 <%def name="menu_bar_nav()">
17 17 ${self.menu_items(active='admin')}
18 18 </%def>
19 19
20 20 <%def name="main()">
21 21 <div class="box">
22 22
23 23 <div class="title">
24 24 ${self.breadcrumbs()}
25 25 <ul class="links">
26 26 %if h.HasPermissionAny('hg.admin', 'hg.usergroup.create.true')():
27 27 <li>
28 28 <a href="${h.url('new_users_group')}" class="btn btn-small btn-success">${_(u'Add User Group')}</a>
29 29 </li>
30 30 %endif
31 31 </ul>
32 32 </div>
33 33
34 34 <div id="repos_list_wrap">
35 35 <table id="user_group_list_table" class="display"></table>
36 36 </div>
37 37
38 38 </div>
39 39 <script>
40 40 $(document).ready(function() {
41 var getDatatableCount = function(){
42 var table = $('#user_group_list_table').dataTable();
43 var page = table.api().page.info();
44 var active = page.recordsDisplay;
45 var total = page.recordsTotal;
41 46
42 var get_datatable_count = function(){
43 var api = $('#user_group_list_table').dataTable().api();
44 $('#user_group_count').text(api.page.info().recordsDisplay);
47 var _text = _gettext("{0} out of {1} users").format(active, total);
48 $('#user_group_count').text(_text);
45 49 };
46 50
47 51 // user list
48 52 $('#user_group_list_table').DataTable({
49 data: ${c.data|n},
53 processing: true,
54 serverSide: true,
55 ajax: "${h.route_path('user_groups_data')}",
50 56 dom: 'rtp',
51 57 pageLength: ${c.visual.admin_grid_items},
52 58 order: [[ 0, "asc" ]],
53 59 columns: [
54 { data: {"_": "group_name",
55 "sort": "group_name_raw"}, title: "${_('Name')}", className: "td-componentname" },
56 { data: {"_": "desc",
57 "sort": "desc"}, title: "${_('Description')}", className: "td-description" },
60 { data: {"_": "users_group_name",
61 "sort": "users_group_name"}, title: "${_('Name')}", className: "td-componentname" },
62 { data: {"_": "description",
63 "sort": "description"}, title: "${_('Description')}", className: "td-description" },
58 64 { data: {"_": "members",
59 "sort": "members",
60 "type": Number}, title: "${_('Members')}", className: "td-number" },
65 "sort": "members"}, title: "${_('Members')}", className: "td-number" },
61 66 { data: {"_": "sync",
62 67 "sort": "sync"}, title: "${_('Sync')}", className: "td-sync" },
63 68 { data: {"_": "active",
64 69 "sort": "active"}, title: "${_('Active')}", className: "td-active" },
65 70 { data: {"_": "owner",
66 71 "sort": "owner"}, title: "${_('Owner')}", className: "td-user" },
67 72 { data: {"_": "action",
68 "sort": "action"}, title: "${_('Action')}", className: "td-action" }
73 "sort": "action"}, title: "${_('Action')}", className: "td-action", orderable: false}
69 74 ],
70 75 language: {
71 76 paginate: DEFAULT_GRID_PAGINATION,
77 sProcessing: _gettext('loading...'),
72 78 emptyTable: _gettext("No user groups available yet.")
73 },
74 "initComplete": function( settings, json ) {
75 get_datatable_count();
76 79 }
77 80 });
78 81
79 // update the counter when doing search
80 $('#user_group_list_table').on( 'search.dt', function (e,settings) {
81 get_datatable_count();
82 $('#user_group_list_table').on('xhr.dt', function(e, settings, json, xhr){
83 $('#user_group_list_table').css('opacity', 1);
84 });
85
86 $('#user_group_list_table').on('preXhr.dt', function(e, settings, data){
87 $('#user_group_list_table').css('opacity', 0.3);
82 88 });
83 89
84 // filter, filter both grids
85 $('#q_filter').on( 'keyup', function () {
86 var user_api = $('#user_group_list_table').dataTable().api();
87 user_api
88 .columns(0)
89 .search(this.value)
90 .draw();
90 // refresh counters on draw
91 $('#user_group_list_table').on('draw.dt', function(){
92 getDatatableCount();
91 93 });
92 94
93 // refilter table if page load via back button
94 $("#q_filter").trigger('keyup');
95 // filter
96 $('#q_filter').on('keyup',
97 $.debounce(250, function() {
98 $('#user_group_list_table').DataTable().search(
99 $('#q_filter').val()
100 ).draw();
101 })
102 );
95 103
96 104 });
97 105
98 106 </script>
99 107
100 108 </%def>
@@ -1,270 +1,225 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode.tests import (
24 24 TestController, url, assert_session_flash, link_to, TEST_USER_ADMIN_LOGIN)
25 25 from rhodecode.model.db import User, UserGroup
26 26 from rhodecode.model.meta import Session
27 27 from rhodecode.tests.fixture import Fixture
28 28
29 29 TEST_USER_GROUP = 'admins_test'
30 30
31 31 fixture = Fixture()
32 32
33 33
34 34 class TestAdminUsersGroupsController(TestController):
35 35
36 def test_index(self):
37 self.log_user()
38 response = self.app.get(url('users_groups'))
39 assert response.status_int == 200
40
41 36 def test_create(self):
42 37 self.log_user()
43 38 users_group_name = TEST_USER_GROUP
44 39 response = self.app.post(url('users_groups'), {
45 40 'users_group_name': users_group_name,
46 41 'user_group_description': 'DESC',
47 42 'active': True,
48 43 'csrf_token': self.csrf_token})
49 44
50 45 user_group_link = link_to(
51 46 users_group_name,
52 47 url('edit_users_group',
53 48 user_group_id=UserGroup.get_by_group_name(
54 49 users_group_name).users_group_id))
55 50 assert_session_flash(
56 51 response,
57 52 'Created user group %s' % user_group_link)
58 53
59 54 def test_set_synchronization(self):
60 55 self.log_user()
61 56 users_group_name = TEST_USER_GROUP + 'sync'
62 57 response = self.app.post(url('users_groups'), {
63 58 'users_group_name': users_group_name,
64 59 'user_group_description': 'DESC',
65 60 'active': True,
66 61 'csrf_token': self.csrf_token})
67 62
68 63 group = Session().query(UserGroup).filter(
69 64 UserGroup.users_group_name == users_group_name).one()
70 65
71 66 assert group.group_data.get('extern_type') is None
72 67
73 68 # enable
74 69 self.app.post(
75 70 url('edit_user_group_advanced_sync', user_group_id=group.users_group_id),
76 71 params={'csrf_token': self.csrf_token}, status=302)
77 72
78 73 group = Session().query(UserGroup).filter(
79 74 UserGroup.users_group_name == users_group_name).one()
80 75 assert group.group_data.get('extern_type') == 'manual'
81 76 assert group.group_data.get('extern_type_set_by') == TEST_USER_ADMIN_LOGIN
82 77
83 78 # disable
84 79 self.app.post(
85 80 url('edit_user_group_advanced_sync',
86 81 user_group_id=group.users_group_id),
87 82 params={'csrf_token': self.csrf_token}, status=302)
88 83
89 84 group = Session().query(UserGroup).filter(
90 85 UserGroup.users_group_name == users_group_name).one()
91 86 assert group.group_data.get('extern_type') is None
92 87 assert group.group_data.get('extern_type_set_by') == TEST_USER_ADMIN_LOGIN
93 88
94 89 def test_delete(self):
95 90 self.log_user()
96 91 users_group_name = TEST_USER_GROUP + 'another'
97 92 response = self.app.post(url('users_groups'), {
98 93 'users_group_name': users_group_name,
99 94 'user_group_description': 'DESC',
100 95 'active': True,
101 96 'csrf_token': self.csrf_token})
102 97
103 98 user_group_link = link_to(
104 99 users_group_name,
105 100 url('edit_users_group',
106 101 user_group_id=UserGroup.get_by_group_name(
107 102 users_group_name).users_group_id))
108 103 assert_session_flash(
109 104 response,
110 105 'Created user group %s' % user_group_link)
111 106
112 107 group = Session().query(UserGroup).filter(
113 108 UserGroup.users_group_name == users_group_name).one()
114 109
115 110 self.app.post(
116 111 url('delete_users_group', user_group_id=group.users_group_id),
117 112 params={'_method': 'delete', 'csrf_token': self.csrf_token})
118 113
119 114 group = Session().query(UserGroup).filter(
120 115 UserGroup.users_group_name == users_group_name).scalar()
121 116
122 117 assert group is None
123 118
124 119 @pytest.mark.parametrize('repo_create, repo_create_write, user_group_create, repo_group_create, fork_create, inherit_default_permissions, expect_error, expect_form_error', [
125 120 ('hg.create.none', 'hg.create.write_on_repogroup.false', 'hg.usergroup.create.false', 'hg.repogroup.create.false', 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
126 121 ('hg.create.repository', 'hg.create.write_on_repogroup.true', 'hg.usergroup.create.true', 'hg.repogroup.create.true', 'hg.fork.repository', 'hg.inherit_default_perms.false', False, False),
127 122 ('hg.create.XXX', 'hg.create.write_on_repogroup.true', 'hg.usergroup.create.true', 'hg.repogroup.create.true', 'hg.fork.repository', 'hg.inherit_default_perms.false', False, True),
128 123 ('', '', '', '', '', '', True, False),
129 124 ])
130 125 def test_global_perms_on_group(
131 126 self, repo_create, repo_create_write, user_group_create,
132 127 repo_group_create, fork_create, expect_error, expect_form_error,
133 128 inherit_default_permissions):
134 129 self.log_user()
135 130 users_group_name = TEST_USER_GROUP + 'another2'
136 131 response = self.app.post(url('users_groups'),
137 132 {'users_group_name': users_group_name,
138 133 'user_group_description': 'DESC',
139 134 'active': True,
140 135 'csrf_token': self.csrf_token})
141 136
142 137 ug = UserGroup.get_by_group_name(users_group_name)
143 138 user_group_link = link_to(
144 139 users_group_name,
145 140 url('edit_users_group', user_group_id=ug.users_group_id))
146 141 assert_session_flash(
147 142 response,
148 143 'Created user group %s' % user_group_link)
149 144 response.follow()
150 145
151 146 # ENABLE REPO CREATE ON A GROUP
152 147 perm_params = {
153 148 'inherit_default_permissions': False,
154 149 'default_repo_create': repo_create,
155 150 'default_repo_create_on_write': repo_create_write,
156 151 'default_user_group_create': user_group_create,
157 152 'default_repo_group_create': repo_group_create,
158 153 'default_fork_create': fork_create,
159 154 'default_inherit_default_permissions': inherit_default_permissions,
160 155
161 156 '_method': 'put',
162 157 'csrf_token': self.csrf_token,
163 158 }
164 159 response = self.app.post(
165 160 url('edit_user_group_global_perms',
166 161 user_group_id=ug.users_group_id),
167 162 params=perm_params)
168 163
169 164 if expect_form_error:
170 165 assert response.status_int == 200
171 166 response.mustcontain('Value must be one of')
172 167 else:
173 168 if expect_error:
174 169 msg = 'An error occurred during permissions saving'
175 170 else:
176 171 msg = 'User Group global permissions updated successfully'
177 172 ug = UserGroup.get_by_group_name(users_group_name)
178 173 del perm_params['_method']
179 174 del perm_params['csrf_token']
180 175 del perm_params['inherit_default_permissions']
181 176 assert perm_params == ug.get_default_perms()
182 177 assert_session_flash(response, msg)
183 178
184 179 fixture.destroy_user_group(users_group_name)
185 180
186 181 def test_edit_autocomplete(self):
187 182 self.log_user()
188 183 ug = fixture.create_user_group(TEST_USER_GROUP, skip_if_exists=True)
189 184 response = self.app.get(
190 185 url('edit_users_group', user_group_id=ug.users_group_id))
191 186 fixture.destroy_user_group(TEST_USER_GROUP)
192 187
193 def test_edit_user_group_autocomplete_members(self, xhr_header):
194 self.log_user()
195 ug = fixture.create_user_group(TEST_USER_GROUP, skip_if_exists=True)
196 response = self.app.get(
197 url('edit_user_group_members', user_group_id=ug.users_group_id),
198 extra_environ=xhr_header)
199
200 assert response.body == '{"members": []}'
201 fixture.destroy_user_group(TEST_USER_GROUP)
202
203 def test_usergroup_escape(self, user_util):
204 user = user_util.create_user(
205 username='escape_user',
206 firstname='<img src="/image2" onload="alert(\'Hello, World!\');">',
207 lastname='<img src="/image2" onload="alert(\'Hello, World!\');">'
208 )
209
210 user_util.create_user_group(owner=user.username)
211
212 self.log_user()
213 users_group_name = 'samplegroup'
214 data = {
215 'users_group_name': users_group_name,
216 'user_group_description': (
217 '<strong onload="alert();">DESC</strong>'),
218 'active': True,
219 'csrf_token': self.csrf_token
220 }
221
222 self.app.post(url('users_groups'), data)
223 response = self.app.get(url('users_groups'))
224
225 response.mustcontain(
226 '&lt;strong onload=&#34;alert();&#34;&gt;'
227 'DESC&lt;/strong&gt;')
228 # TODO(marcink): fix this test after user-group grid rewrite
229 # response.mustcontain(
230 # '&lt;img src=&#34;/image2&#34; onload=&#34;'
231 # 'alert(&#39;Hello, World!&#39;);&#34;&gt;')
232
233 188 def test_update_members_from_user_ids(self, user_regular):
234 189 uid = user_regular.user_id
235 190 username = user_regular.username
236 191 self.log_user()
237 192
238 193 user_group = fixture.create_user_group('test_gr_ids')
239 194 assert user_group.members == []
240 195 assert user_group.user != user_regular
241 196 expected_active_state = not user_group.users_group_active
242 197
243 198 form_data = [
244 199 ('csrf_token', self.csrf_token),
245 200 ('_method', 'put'),
246 201 ('user', username),
247 202 ('users_group_name', 'changed_name'),
248 203 ('users_group_active', expected_active_state),
249 204 ('user_group_description', 'changed_description'),
250 205
251 206 ('__start__', 'user_group_members:sequence'),
252 207 ('__start__', 'member:mapping'),
253 208 ('member_user_id', uid),
254 209 ('type', 'existing'),
255 210 ('__end__', 'member:mapping'),
256 211 ('__end__', 'user_group_members:sequence'),
257 212 ]
258 213 ugid = user_group.users_group_id
259 214 self.app.post(url('update_users_group', user_group_id=ugid), form_data)
260 215
261 216 user_group = UserGroup.get(ugid)
262 217 assert user_group
263 218
264 219 assert user_group.members[0].user_id == uid
265 220 assert user_group.user_id == uid
266 221 assert 'changed_name' in user_group.users_group_name
267 222 assert 'changed_description' in user_group.user_group_description
268 223 assert user_group.users_group_active == expected_active_state
269 224
270 225 fixture.destroy_user_group(user_group)
@@ -1,134 +1,151 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode.tests import (
24 24 TestController, url, assert_session_flash, link_to)
25 25 from rhodecode.model.db import User, UserGroup
26 26 from rhodecode.model.meta import Session
27 27 from rhodecode.tests.fixture import Fixture
28 28
29 29
30 def route_path(name, params=None, **kwargs):
31 import urllib
32 from rhodecode.apps._base import ADMIN_PREFIX
33
34 base_url = {
35 'home': '/',
36 'user_groups':
37 ADMIN_PREFIX + '/user_groups',
38 'user_groups_data':
39 ADMIN_PREFIX + '/user_groups_data',
40 }[name].format(**kwargs)
41
42 if params:
43 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
44 return base_url
45
46
30 47 fixture = Fixture()
31 48
32 49
33 def route_path(name, **kwargs):
34 return {
35 'home': '/',
36 }[name].format(**kwargs)
50 class TestAdminDelegatedUser(TestController):
37 51
38
39 class TestAdminUsersGroupsController(TestController):
40
41 def test_regular_user_cannot_see_admin_interfaces(self, user_util):
52 def test_regular_user_cannot_see_admin_interfaces(
53 self, user_util, xhr_header):
42 54 user = user_util.create_user(password='qweqwe')
43 55 self.log_user(user.username, 'qweqwe')
44 56
45 57 # check if in home view, such user doesn't see the "admin" menus
46 58 response = self.app.get(route_path('home'))
47 59
48 60 assert_response = response.assert_response()
49 61
50 62 assert_response.no_element_exists('li.local-admin-repos')
51 63 assert_response.no_element_exists('li.local-admin-repo-groups')
52 64 assert_response.no_element_exists('li.local-admin-user-groups')
53 65
54 66 response = self.app.get(url('repos'), status=200)
55 67 response.mustcontain('data: []')
56 68
57 69 response = self.app.get(url('repo_groups'), status=200)
58 70 response.mustcontain('data: []')
59 71
60 response = self.app.get(url('users_groups'), status=200)
61 response.mustcontain('data: []')
72 response = self.app.get(route_path('user_groups_data'),
73 status=200, extra_environ=xhr_header)
74 assert response.json['data'] == []
62 75
63 def test_regular_user_can_see_admin_interfaces_if_owner(self, user_util):
76 def test_regular_user_can_see_admin_interfaces_if_owner(
77 self, user_util, xhr_header):
64 78 user = user_util.create_user(password='qweqwe')
65 79 username = user.username
66 80
67 81 repo = user_util.create_repo(owner=username)
68 82 repo_name = repo.repo_name
69 83
70 84 repo_group = user_util.create_repo_group(owner=username)
71 85 repo_group_name = repo_group.group_name
72 86
73 87 user_group = user_util.create_user_group(owner=username)
74 88 user_group_name = user_group.users_group_name
75 89
76 90 self.log_user(username, 'qweqwe')
77 91 # check if in home view, such user doesn't see the "admin" menus
78 92 response = self.app.get(route_path('home'))
79 93
80 94 assert_response = response.assert_response()
81 95
82 96 assert_response.one_element_exists('li.local-admin-repos')
83 97 assert_response.one_element_exists('li.local-admin-repo-groups')
84 98 assert_response.one_element_exists('li.local-admin-user-groups')
85 99
86 100 # admin interfaces have visible elements
87 101 response = self.app.get(url('repos'), status=200)
88 102 response.mustcontain('"name_raw": "{}"'.format(repo_name))
89 103
90 104 response = self.app.get(url('repo_groups'), status=200)
91 105 response.mustcontain('"name_raw": "{}"'.format(repo_group_name))
92 106
93 response = self.app.get(url('users_groups'), status=200)
94 response.mustcontain('"group_name_raw": "{}"'.format(user_group_name))
107 response = self.app.get(route_path('user_groups_data'),
108 extra_environ=xhr_header, status=200)
109 response.mustcontain('"name_raw": "{}"'.format(user_group_name))
95 110
96 def test_regular_user_can_see_admin_interfaces_if_admin_perm(self, user_util):
111 def test_regular_user_can_see_admin_interfaces_if_admin_perm(
112 self, user_util, xhr_header):
97 113 user = user_util.create_user(password='qweqwe')
98 114 username = user.username
99 115
100 116 repo = user_util.create_repo()
101 117 repo_name = repo.repo_name
102 118
103 119 repo_group = user_util.create_repo_group()
104 120 repo_group_name = repo_group.group_name
105 121
106 122 user_group = user_util.create_user_group()
107 123 user_group_name = user_group.users_group_name
108 124
109 125 user_util.grant_user_permission_to_repo(
110 126 repo, user, 'repository.admin')
111 127 user_util.grant_user_permission_to_repo_group(
112 128 repo_group, user, 'group.admin')
113 129 user_util.grant_user_permission_to_user_group(
114 130 user_group, user, 'usergroup.admin')
115 131
116 132 self.log_user(username, 'qweqwe')
117 133 # check if in home view, such user doesn't see the "admin" menus
118 134 response = self.app.get(route_path('home'))
119 135
120 136 assert_response = response.assert_response()
121 137
122 138 assert_response.one_element_exists('li.local-admin-repos')
123 139 assert_response.one_element_exists('li.local-admin-repo-groups')
124 140 assert_response.one_element_exists('li.local-admin-user-groups')
125 141
126 142 # admin interfaces have visible elements
127 143 response = self.app.get(url('repos'), status=200)
128 144 response.mustcontain('"name_raw": "{}"'.format(repo_name))
129 145
130 146 response = self.app.get(url('repo_groups'), status=200)
131 147 response.mustcontain('"name_raw": "{}"'.format(repo_group_name))
132 148
133 response = self.app.get(url('users_groups'), status=200)
134 response.mustcontain('"group_name_raw": "{}"'.format(user_group_name))
149 response = self.app.get(route_path('user_groups_data'),
150 extra_environ=xhr_header, status=200)
151 response.mustcontain('"name_raw": "{}"'.format(user_group_name))
General Comments 0
You need to be logged in to leave comments. Login now