##// END OF EJS Templates
feat(login by email option): added ability to log in with user primary email. Fixes: RCCE-63
ilin.s -
r5358:2095c653 default
parent child Browse files
Show More
@@ -1,581 +1,593 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import urllib.parse
19 import urllib.parse
20
20
21 import mock
21 import mock
22 import pytest
22 import pytest
23
23
24
24
25 from rhodecode.lib.auth import check_password
25 from rhodecode.lib.auth import check_password
26 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
27 from rhodecode.model.auth_token import AuthTokenModel
27 from rhodecode.model.auth_token import AuthTokenModel
28 from rhodecode.model.db import User, Notification, UserApiKeys
28 from rhodecode.model.db import User, Notification, UserApiKeys
29 from rhodecode.model.meta import Session
29 from rhodecode.model.meta import Session
30
30
31 from rhodecode.tests import (
31 from rhodecode.tests import (
32 assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN,
32 assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN,
33 no_newline_id_generator)
33 no_newline_id_generator)
34 from rhodecode.tests.fixture import Fixture
34 from rhodecode.tests.fixture import Fixture
35 from rhodecode.tests.routes import route_path
35 from rhodecode.tests.routes import route_path
36
36
37 fixture = Fixture()
37 fixture = Fixture()
38
38
39 whitelist_view = ['RepoCommitsView:repo_commit_raw']
39 whitelist_view = ['RepoCommitsView:repo_commit_raw']
40
40
41
41
42 @pytest.mark.usefixtures('app')
42 @pytest.mark.usefixtures('app')
43 class TestLoginController(object):
43 class TestLoginController(object):
44 destroy_users = set()
44 destroy_users = set()
45
45
46 @classmethod
46 @classmethod
47 def teardown_class(cls):
47 def teardown_class(cls):
48 fixture.destroy_users(cls.destroy_users)
48 fixture.destroy_users(cls.destroy_users)
49
49
50 def teardown_method(self, method):
50 def teardown_method(self, method):
51 for n in Notification.query().all():
51 for n in Notification.query().all():
52 Session().delete(n)
52 Session().delete(n)
53
53
54 Session().commit()
54 Session().commit()
55 assert Notification.query().all() == []
55 assert Notification.query().all() == []
56
56
57 def test_index(self):
57 def test_index(self):
58 response = self.app.get(route_path('login'))
58 response = self.app.get(route_path('login'))
59 assert response.status == '200 OK'
59 assert response.status == '200 OK'
60 # Test response...
60 # Test response...
61
61
62 def test_login_admin_ok(self):
62 def test_login_admin_ok(self):
63 response = self.app.post(route_path('login'),
63 response = self.app.post(route_path('login'),
64 {'username': 'test_admin',
64 {'username': 'test_admin',
65 'password': 'test12'}, status=302)
65 'password': 'test12'}, status=302)
66 response = response.follow()
66 response = response.follow()
67 session = response.get_session_from_response()
67 session = response.get_session_from_response()
68 username = session['rhodecode_user'].get('username')
68 username = session['rhodecode_user'].get('username')
69 assert username == 'test_admin'
69 assert username == 'test_admin'
70 response.mustcontain('logout')
70 response.mustcontain('logout')
71
71
72 def test_login_regular_ok(self):
72 def test_login_regular_ok(self):
73 response = self.app.post(route_path('login'),
73 response = self.app.post(route_path('login'),
74 {'username': 'test_regular',
74 {'username': 'test_regular',
75 'password': 'test12'}, status=302)
75 'password': 'test12'}, status=302)
76
76
77 response = response.follow()
77 response = response.follow()
78 session = response.get_session_from_response()
78 session = response.get_session_from_response()
79 username = session['rhodecode_user'].get('username')
79 username = session['rhodecode_user'].get('username')
80 assert username == 'test_regular'
80 assert username == 'test_regular'
81 response.mustcontain('logout')
81 response.mustcontain('logout')
82
82
83 def test_login_with_primary_email(self):
84 user_email = 'test_regular@mail.com'
85 response = self.app.post(route_path('login'),
86 {'username': user_email,
87 'password': 'test12'}, status=302)
88 response = response.follow()
89 session = response.get_session_from_response()
90 user = session['rhodecode_user']
91 assert user['username'] == user_email.split('@')[0]
92 assert user['is_authenticated']
93 response.mustcontain('logout')
94
83 def test_login_regular_forbidden_when_super_admin_restriction(self):
95 def test_login_regular_forbidden_when_super_admin_restriction(self):
84 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
96 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
85 with fixture.auth_restriction(self.app._pyramid_registry,
97 with fixture.auth_restriction(self.app._pyramid_registry,
86 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN):
98 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN):
87 response = self.app.post(route_path('login'),
99 response = self.app.post(route_path('login'),
88 {'username': 'test_regular',
100 {'username': 'test_regular',
89 'password': 'test12'})
101 'password': 'test12'})
90
102
91 response.mustcontain('invalid user name')
103 response.mustcontain('invalid user name')
92 response.mustcontain('invalid password')
104 response.mustcontain('invalid password')
93
105
94 def test_login_regular_forbidden_when_scope_restriction(self):
106 def test_login_regular_forbidden_when_scope_restriction(self):
95 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
107 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
96 with fixture.scope_restriction(self.app._pyramid_registry,
108 with fixture.scope_restriction(self.app._pyramid_registry,
97 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_VCS):
109 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_VCS):
98 response = self.app.post(route_path('login'),
110 response = self.app.post(route_path('login'),
99 {'username': 'test_regular',
111 {'username': 'test_regular',
100 'password': 'test12'})
112 'password': 'test12'})
101
113
102 response.mustcontain('invalid user name')
114 response.mustcontain('invalid user name')
103 response.mustcontain('invalid password')
115 response.mustcontain('invalid password')
104
116
105 def test_login_ok_came_from(self):
117 def test_login_ok_came_from(self):
106 test_came_from = '/_admin/users?branch=stable'
118 test_came_from = '/_admin/users?branch=stable'
107 _url = '{}?came_from={}'.format(route_path('login'), test_came_from)
119 _url = '{}?came_from={}'.format(route_path('login'), test_came_from)
108 response = self.app.post(
120 response = self.app.post(
109 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
121 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
110
122
111 assert 'branch=stable' in response.location
123 assert 'branch=stable' in response.location
112 response = response.follow()
124 response = response.follow()
113
125
114 assert response.status == '200 OK'
126 assert response.status == '200 OK'
115 response.mustcontain('Users administration')
127 response.mustcontain('Users administration')
116
128
117 def test_redirect_to_login_with_get_args(self):
129 def test_redirect_to_login_with_get_args(self):
118 with fixture.anon_access(False):
130 with fixture.anon_access(False):
119 kwargs = {'branch': 'stable'}
131 kwargs = {'branch': 'stable'}
120 response = self.app.get(
132 response = self.app.get(
121 h.route_path('repo_summary', repo_name=HG_REPO, _query=kwargs),
133 h.route_path('repo_summary', repo_name=HG_REPO, _query=kwargs),
122 status=302)
134 status=302)
123
135
124 response_query = urllib.parse.parse_qsl(response.location)
136 response_query = urllib.parse.parse_qsl(response.location)
125 assert 'branch=stable' in response_query[0][1]
137 assert 'branch=stable' in response_query[0][1]
126
138
127 def test_login_form_with_get_args(self):
139 def test_login_form_with_get_args(self):
128 _url = '{}?came_from=/_admin/users,branch=stable'.format(route_path('login'))
140 _url = '{}?came_from=/_admin/users,branch=stable'.format(route_path('login'))
129 response = self.app.get(_url)
141 response = self.app.get(_url)
130 assert 'branch%3Dstable' in response.form.action
142 assert 'branch%3Dstable' in response.form.action
131
143
132 @pytest.mark.parametrize("url_came_from", [
144 @pytest.mark.parametrize("url_came_from", [
133 'data:text/html,<script>window.alert("xss")</script>',
145 'data:text/html,<script>window.alert("xss")</script>',
134 'mailto:test@rhodecode.org',
146 'mailto:test@rhodecode.org',
135 'file:///etc/passwd',
147 'file:///etc/passwd',
136 'ftp://some.ftp.server',
148 'ftp://some.ftp.server',
137 'http://other.domain',
149 'http://other.domain',
138 ], ids=no_newline_id_generator)
150 ], ids=no_newline_id_generator)
139 def test_login_bad_came_froms(self, url_came_from):
151 def test_login_bad_came_froms(self, url_came_from):
140 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
152 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
141 response = self.app.post(
153 response = self.app.post(
142 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
154 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
143 assert response.status == '302 Found'
155 assert response.status == '302 Found'
144 response = response.follow()
156 response = response.follow()
145 assert response.status == '200 OK'
157 assert response.status == '200 OK'
146 assert response.request.path == '/'
158 assert response.request.path == '/'
147
159
148 @pytest.mark.xfail(reason="newline params changed behaviour in python3")
160 @pytest.mark.xfail(reason="newline params changed behaviour in python3")
149 @pytest.mark.parametrize("url_came_from", [
161 @pytest.mark.parametrize("url_came_from", [
150 '/\r\nX-Forwarded-Host: \rhttp://example.org',
162 '/\r\nX-Forwarded-Host: \rhttp://example.org',
151 ], ids=no_newline_id_generator)
163 ], ids=no_newline_id_generator)
152 def test_login_bad_came_froms_404(self, url_came_from):
164 def test_login_bad_came_froms_404(self, url_came_from):
153 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
165 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
154 response = self.app.post(
166 response = self.app.post(
155 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
167 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
156
168
157 response = response.follow()
169 response = response.follow()
158 assert response.status == '404 Not Found'
170 assert response.status == '404 Not Found'
159
171
160 def test_login_short_password(self):
172 def test_login_short_password(self):
161 response = self.app.post(route_path('login'),
173 response = self.app.post(route_path('login'),
162 {'username': 'test_admin',
174 {'username': 'test_admin',
163 'password': 'as'})
175 'password': 'as'})
164 assert response.status == '200 OK'
176 assert response.status == '200 OK'
165
177
166 response.mustcontain('Enter 3 characters or more')
178 response.mustcontain('Enter 3 characters or more')
167
179
168 def test_login_wrong_non_ascii_password(self, user_regular):
180 def test_login_wrong_non_ascii_password(self, user_regular):
169 response = self.app.post(
181 response = self.app.post(
170 route_path('login'),
182 route_path('login'),
171 {'username': user_regular.username,
183 {'username': user_regular.username,
172 'password': 'invalid-non-asci\xe4'.encode('utf8')})
184 'password': 'invalid-non-asci\xe4'.encode('utf8')})
173
185
174 response.mustcontain('invalid user name')
186 response.mustcontain('invalid user name')
175 response.mustcontain('invalid password')
187 response.mustcontain('invalid password')
176
188
177 def test_login_with_non_ascii_password(self, user_util):
189 def test_login_with_non_ascii_password(self, user_util):
178 password = u'valid-non-ascii\xe4'
190 password = u'valid-non-ascii\xe4'
179 user = user_util.create_user(password=password)
191 user = user_util.create_user(password=password)
180 response = self.app.post(
192 response = self.app.post(
181 route_path('login'),
193 route_path('login'),
182 {'username': user.username,
194 {'username': user.username,
183 'password': password})
195 'password': password})
184 assert response.status_code == 302
196 assert response.status_code == 302
185
197
186 def test_login_wrong_username_password(self):
198 def test_login_wrong_username_password(self):
187 response = self.app.post(route_path('login'),
199 response = self.app.post(route_path('login'),
188 {'username': 'error',
200 {'username': 'error',
189 'password': 'test12'})
201 'password': 'test12'})
190
202
191 response.mustcontain('invalid user name')
203 response.mustcontain('invalid user name')
192 response.mustcontain('invalid password')
204 response.mustcontain('invalid password')
193
205
194 def test_login_admin_ok_password_migration(self, real_crypto_backend):
206 def test_login_admin_ok_password_migration(self, real_crypto_backend):
195 from rhodecode.lib import auth
207 from rhodecode.lib import auth
196
208
197 # create new user, with sha256 password
209 # create new user, with sha256 password
198 temp_user = 'test_admin_sha256'
210 temp_user = 'test_admin_sha256'
199 user = fixture.create_user(temp_user)
211 user = fixture.create_user(temp_user)
200 user.password = auth._RhodeCodeCryptoSha256().hash_create(
212 user.password = auth._RhodeCodeCryptoSha256().hash_create(
201 b'test123')
213 b'test123')
202 Session().add(user)
214 Session().add(user)
203 Session().commit()
215 Session().commit()
204 self.destroy_users.add(temp_user)
216 self.destroy_users.add(temp_user)
205 response = self.app.post(route_path('login'),
217 response = self.app.post(route_path('login'),
206 {'username': temp_user,
218 {'username': temp_user,
207 'password': 'test123'}, status=302)
219 'password': 'test123'}, status=302)
208
220
209 response = response.follow()
221 response = response.follow()
210 session = response.get_session_from_response()
222 session = response.get_session_from_response()
211 username = session['rhodecode_user'].get('username')
223 username = session['rhodecode_user'].get('username')
212 assert username == temp_user
224 assert username == temp_user
213 response.mustcontain('logout')
225 response.mustcontain('logout')
214
226
215 # new password should be bcrypted, after log-in and transfer
227 # new password should be bcrypted, after log-in and transfer
216 user = User.get_by_username(temp_user)
228 user = User.get_by_username(temp_user)
217 assert user.password.startswith('$')
229 assert user.password.startswith('$')
218
230
219 # REGISTRATIONS
231 # REGISTRATIONS
220 def test_register(self):
232 def test_register(self):
221 response = self.app.get(route_path('register'))
233 response = self.app.get(route_path('register'))
222 response.mustcontain('Create an Account')
234 response.mustcontain('Create an Account')
223
235
224 def test_register_err_same_username(self):
236 def test_register_err_same_username(self):
225 uname = 'test_admin'
237 uname = 'test_admin'
226 response = self.app.post(
238 response = self.app.post(
227 route_path('register'),
239 route_path('register'),
228 {
240 {
229 'username': uname,
241 'username': uname,
230 'password': 'test12',
242 'password': 'test12',
231 'password_confirmation': 'test12',
243 'password_confirmation': 'test12',
232 'email': 'goodmail@domain.com',
244 'email': 'goodmail@domain.com',
233 'firstname': 'test',
245 'firstname': 'test',
234 'lastname': 'test'
246 'lastname': 'test'
235 }
247 }
236 )
248 )
237
249
238 assertr = response.assert_response()
250 assertr = response.assert_response()
239 msg = 'Username "%(username)s" already exists'
251 msg = 'Username "%(username)s" already exists'
240 msg = msg % {'username': uname}
252 msg = msg % {'username': uname}
241 assertr.element_contains('#username+.error-message', msg)
253 assertr.element_contains('#username+.error-message', msg)
242
254
243 def test_register_err_same_email(self):
255 def test_register_err_same_email(self):
244 response = self.app.post(
256 response = self.app.post(
245 route_path('register'),
257 route_path('register'),
246 {
258 {
247 'username': 'test_admin_0',
259 'username': 'test_admin_0',
248 'password': 'test12',
260 'password': 'test12',
249 'password_confirmation': 'test12',
261 'password_confirmation': 'test12',
250 'email': 'test_admin@mail.com',
262 'email': 'test_admin@mail.com',
251 'firstname': 'test',
263 'firstname': 'test',
252 'lastname': 'test'
264 'lastname': 'test'
253 }
265 }
254 )
266 )
255
267
256 assertr = response.assert_response()
268 assertr = response.assert_response()
257 msg = u'This e-mail address is already taken'
269 msg = u'This e-mail address is already taken'
258 assertr.element_contains('#email+.error-message', msg)
270 assertr.element_contains('#email+.error-message', msg)
259
271
260 def test_register_err_same_email_case_sensitive(self):
272 def test_register_err_same_email_case_sensitive(self):
261 response = self.app.post(
273 response = self.app.post(
262 route_path('register'),
274 route_path('register'),
263 {
275 {
264 'username': 'test_admin_1',
276 'username': 'test_admin_1',
265 'password': 'test12',
277 'password': 'test12',
266 'password_confirmation': 'test12',
278 'password_confirmation': 'test12',
267 'email': 'TesT_Admin@mail.COM',
279 'email': 'TesT_Admin@mail.COM',
268 'firstname': 'test',
280 'firstname': 'test',
269 'lastname': 'test'
281 'lastname': 'test'
270 }
282 }
271 )
283 )
272 assertr = response.assert_response()
284 assertr = response.assert_response()
273 msg = u'This e-mail address is already taken'
285 msg = u'This e-mail address is already taken'
274 assertr.element_contains('#email+.error-message', msg)
286 assertr.element_contains('#email+.error-message', msg)
275
287
276 def test_register_err_wrong_data(self):
288 def test_register_err_wrong_data(self):
277 response = self.app.post(
289 response = self.app.post(
278 route_path('register'),
290 route_path('register'),
279 {
291 {
280 'username': 'xs',
292 'username': 'xs',
281 'password': 'test',
293 'password': 'test',
282 'password_confirmation': 'test',
294 'password_confirmation': 'test',
283 'email': 'goodmailm',
295 'email': 'goodmailm',
284 'firstname': 'test',
296 'firstname': 'test',
285 'lastname': 'test'
297 'lastname': 'test'
286 }
298 }
287 )
299 )
288 assert response.status == '200 OK'
300 assert response.status == '200 OK'
289 response.mustcontain('An email address must contain a single @')
301 response.mustcontain('An email address must contain a single @')
290 response.mustcontain('Enter a value 6 characters long or more')
302 response.mustcontain('Enter a value 6 characters long or more')
291
303
292 def test_register_err_username(self):
304 def test_register_err_username(self):
293 response = self.app.post(
305 response = self.app.post(
294 route_path('register'),
306 route_path('register'),
295 {
307 {
296 'username': 'error user',
308 'username': 'error user',
297 'password': 'test12',
309 'password': 'test12',
298 'password_confirmation': 'test12',
310 'password_confirmation': 'test12',
299 'email': 'goodmailm',
311 'email': 'goodmailm',
300 'firstname': 'test',
312 'firstname': 'test',
301 'lastname': 'test'
313 'lastname': 'test'
302 }
314 }
303 )
315 )
304
316
305 response.mustcontain('An email address must contain a single @')
317 response.mustcontain('An email address must contain a single @')
306 response.mustcontain(
318 response.mustcontain(
307 'Username may only contain '
319 'Username may only contain '
308 'alphanumeric characters underscores, '
320 'alphanumeric characters underscores, '
309 'periods or dashes and must begin with '
321 'periods or dashes and must begin with '
310 'alphanumeric character')
322 'alphanumeric character')
311
323
312 def test_register_err_case_sensitive(self):
324 def test_register_err_case_sensitive(self):
313 usr = 'Test_Admin'
325 usr = 'Test_Admin'
314 response = self.app.post(
326 response = self.app.post(
315 route_path('register'),
327 route_path('register'),
316 {
328 {
317 'username': usr,
329 'username': usr,
318 'password': 'test12',
330 'password': 'test12',
319 'password_confirmation': 'test12',
331 'password_confirmation': 'test12',
320 'email': 'goodmailm',
332 'email': 'goodmailm',
321 'firstname': 'test',
333 'firstname': 'test',
322 'lastname': 'test'
334 'lastname': 'test'
323 }
335 }
324 )
336 )
325
337
326 assertr = response.assert_response()
338 assertr = response.assert_response()
327 msg = u'Username "%(username)s" already exists'
339 msg = u'Username "%(username)s" already exists'
328 msg = msg % {'username': usr}
340 msg = msg % {'username': usr}
329 assertr.element_contains('#username+.error-message', msg)
341 assertr.element_contains('#username+.error-message', msg)
330
342
331 def test_register_special_chars(self):
343 def test_register_special_chars(self):
332 response = self.app.post(
344 response = self.app.post(
333 route_path('register'),
345 route_path('register'),
334 {
346 {
335 'username': 'xxxaxn',
347 'username': 'xxxaxn',
336 'password': 'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
348 'password': 'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
337 'password_confirmation': 'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
349 'password_confirmation': 'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
338 'email': 'goodmailm@test.plx',
350 'email': 'goodmailm@test.plx',
339 'firstname': 'test',
351 'firstname': 'test',
340 'lastname': 'test'
352 'lastname': 'test'
341 }
353 }
342 )
354 )
343
355
344 msg = u'Invalid characters (non-ascii) in password'
356 msg = u'Invalid characters (non-ascii) in password'
345 response.mustcontain(msg)
357 response.mustcontain(msg)
346
358
347 def test_register_password_mismatch(self):
359 def test_register_password_mismatch(self):
348 response = self.app.post(
360 response = self.app.post(
349 route_path('register'),
361 route_path('register'),
350 {
362 {
351 'username': 'xs',
363 'username': 'xs',
352 'password': '123qwe',
364 'password': '123qwe',
353 'password_confirmation': 'qwe123',
365 'password_confirmation': 'qwe123',
354 'email': 'goodmailm@test.plxa',
366 'email': 'goodmailm@test.plxa',
355 'firstname': 'test',
367 'firstname': 'test',
356 'lastname': 'test'
368 'lastname': 'test'
357 }
369 }
358 )
370 )
359 msg = u'Passwords do not match'
371 msg = u'Passwords do not match'
360 response.mustcontain(msg)
372 response.mustcontain(msg)
361
373
362 def test_register_ok(self):
374 def test_register_ok(self):
363 username = 'test_regular4'
375 username = 'test_regular4'
364 password = 'qweqwe'
376 password = 'qweqwe'
365 email = 'marcin@test.com'
377 email = 'marcin@test.com'
366 name = 'testname'
378 name = 'testname'
367 lastname = 'testlastname'
379 lastname = 'testlastname'
368
380
369 # this initializes a session
381 # this initializes a session
370 response = self.app.get(route_path('register'))
382 response = self.app.get(route_path('register'))
371 response.mustcontain('Create an Account')
383 response.mustcontain('Create an Account')
372
384
373
385
374 response = self.app.post(
386 response = self.app.post(
375 route_path('register'),
387 route_path('register'),
376 {
388 {
377 'username': username,
389 'username': username,
378 'password': password,
390 'password': password,
379 'password_confirmation': password,
391 'password_confirmation': password,
380 'email': email,
392 'email': email,
381 'firstname': name,
393 'firstname': name,
382 'lastname': lastname,
394 'lastname': lastname,
383 'admin': True
395 'admin': True
384 },
396 },
385 status=302
397 status=302
386 ) # This should be overridden
398 ) # This should be overridden
387
399
388 assert_session_flash(
400 assert_session_flash(
389 response, 'You have successfully registered with RhodeCode. You can log-in now.')
401 response, 'You have successfully registered with RhodeCode. You can log-in now.')
390
402
391 ret = Session().query(User).filter(
403 ret = Session().query(User).filter(
392 User.username == 'test_regular4').one()
404 User.username == 'test_regular4').one()
393 assert ret.username == username
405 assert ret.username == username
394 assert check_password(password, ret.password)
406 assert check_password(password, ret.password)
395 assert ret.email == email
407 assert ret.email == email
396 assert ret.name == name
408 assert ret.name == name
397 assert ret.lastname == lastname
409 assert ret.lastname == lastname
398 assert ret.auth_tokens is not None
410 assert ret.auth_tokens is not None
399 assert not ret.admin
411 assert not ret.admin
400
412
401 def test_forgot_password_wrong_mail(self):
413 def test_forgot_password_wrong_mail(self):
402 bad_email = 'marcin@wrongmail.org'
414 bad_email = 'marcin@wrongmail.org'
403 # this initializes a session
415 # this initializes a session
404 self.app.get(route_path('reset_password'))
416 self.app.get(route_path('reset_password'))
405
417
406 response = self.app.post(
418 response = self.app.post(
407 route_path('reset_password'), {'email': bad_email, }
419 route_path('reset_password'), {'email': bad_email, }
408 )
420 )
409 assert_session_flash(response,
421 assert_session_flash(response,
410 'If such email exists, a password reset link was sent to it.')
422 'If such email exists, a password reset link was sent to it.')
411
423
412 def test_forgot_password(self, user_util):
424 def test_forgot_password(self, user_util):
413 # this initializes a session
425 # this initializes a session
414 self.app.get(route_path('reset_password'))
426 self.app.get(route_path('reset_password'))
415
427
416 user = user_util.create_user()
428 user = user_util.create_user()
417 user_id = user.user_id
429 user_id = user.user_id
418 email = user.email
430 email = user.email
419
431
420 response = self.app.post(route_path('reset_password'), {'email': email, })
432 response = self.app.post(route_path('reset_password'), {'email': email, })
421
433
422 assert_session_flash(response,
434 assert_session_flash(response,
423 'If such email exists, a password reset link was sent to it.')
435 'If such email exists, a password reset link was sent to it.')
424
436
425 # BAD KEY
437 # BAD KEY
426 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), 'badkey')
438 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), 'badkey')
427 response = self.app.get(confirm_url, status=302)
439 response = self.app.get(confirm_url, status=302)
428 assert response.location.endswith(route_path('reset_password'))
440 assert response.location.endswith(route_path('reset_password'))
429 assert_session_flash(response, 'Given reset token is invalid')
441 assert_session_flash(response, 'Given reset token is invalid')
430
442
431 response.follow() # cleanup flash
443 response.follow() # cleanup flash
432
444
433 # GOOD KEY
445 # GOOD KEY
434 key = UserApiKeys.query()\
446 key = UserApiKeys.query()\
435 .filter(UserApiKeys.user_id == user_id)\
447 .filter(UserApiKeys.user_id == user_id)\
436 .filter(UserApiKeys.role == UserApiKeys.ROLE_PASSWORD_RESET)\
448 .filter(UserApiKeys.role == UserApiKeys.ROLE_PASSWORD_RESET)\
437 .first()
449 .first()
438
450
439 assert key
451 assert key
440
452
441 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), key.api_key)
453 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), key.api_key)
442 response = self.app.get(confirm_url)
454 response = self.app.get(confirm_url)
443 assert response.status == '302 Found'
455 assert response.status == '302 Found'
444 assert response.location.endswith(route_path('login'))
456 assert response.location.endswith(route_path('login'))
445
457
446 assert_session_flash(
458 assert_session_flash(
447 response,
459 response,
448 'Your password reset was successful, '
460 'Your password reset was successful, '
449 'a new password has been sent to your email')
461 'a new password has been sent to your email')
450
462
451 response.follow()
463 response.follow()
452
464
453 def _get_api_whitelist(self, values=None):
465 def _get_api_whitelist(self, values=None):
454 config = {'api_access_controllers_whitelist': values or []}
466 config = {'api_access_controllers_whitelist': values or []}
455 return config
467 return config
456
468
457 @pytest.mark.parametrize("test_name, auth_token", [
469 @pytest.mark.parametrize("test_name, auth_token", [
458 ('none', None),
470 ('none', None),
459 ('empty_string', ''),
471 ('empty_string', ''),
460 ('fake_number', '123456'),
472 ('fake_number', '123456'),
461 ('proper_auth_token', None)
473 ('proper_auth_token', None)
462 ])
474 ])
463 def test_access_not_whitelisted_page_via_auth_token(
475 def test_access_not_whitelisted_page_via_auth_token(
464 self, test_name, auth_token, user_admin):
476 self, test_name, auth_token, user_admin):
465
477
466 whitelist = self._get_api_whitelist([])
478 whitelist = self._get_api_whitelist([])
467 with mock.patch.dict('rhodecode.CONFIG', whitelist):
479 with mock.patch.dict('rhodecode.CONFIG', whitelist):
468 assert [] == whitelist['api_access_controllers_whitelist']
480 assert [] == whitelist['api_access_controllers_whitelist']
469 if test_name == 'proper_auth_token':
481 if test_name == 'proper_auth_token':
470 # use builtin if api_key is None
482 # use builtin if api_key is None
471 auth_token = user_admin.api_key
483 auth_token = user_admin.api_key
472
484
473 with fixture.anon_access(False):
485 with fixture.anon_access(False):
474 # webtest uses linter to check if response is bytes,
486 # webtest uses linter to check if response is bytes,
475 # and we use memoryview here as a wrapper, quick turn-off
487 # and we use memoryview here as a wrapper, quick turn-off
476 self.app.lint = False
488 self.app.lint = False
477
489
478 self.app.get(
490 self.app.get(
479 route_path('repo_commit_raw',
491 route_path('repo_commit_raw',
480 repo_name=HG_REPO, commit_id='tip',
492 repo_name=HG_REPO, commit_id='tip',
481 params=dict(api_key=auth_token)),
493 params=dict(api_key=auth_token)),
482 status=302)
494 status=302)
483
495
484 @pytest.mark.parametrize("test_name, auth_token, code", [
496 @pytest.mark.parametrize("test_name, auth_token, code", [
485 ('none', None, 302),
497 ('none', None, 302),
486 ('empty_string', '', 302),
498 ('empty_string', '', 302),
487 ('fake_number', '123456', 302),
499 ('fake_number', '123456', 302),
488 ('proper_auth_token', None, 200)
500 ('proper_auth_token', None, 200)
489 ])
501 ])
490 def test_access_whitelisted_page_via_auth_token(
502 def test_access_whitelisted_page_via_auth_token(
491 self, test_name, auth_token, code, user_admin):
503 self, test_name, auth_token, code, user_admin):
492
504
493 whitelist = self._get_api_whitelist(whitelist_view)
505 whitelist = self._get_api_whitelist(whitelist_view)
494
506
495 with mock.patch.dict('rhodecode.CONFIG', whitelist):
507 with mock.patch.dict('rhodecode.CONFIG', whitelist):
496 assert whitelist_view == whitelist['api_access_controllers_whitelist']
508 assert whitelist_view == whitelist['api_access_controllers_whitelist']
497
509
498 if test_name == 'proper_auth_token':
510 if test_name == 'proper_auth_token':
499 auth_token = user_admin.api_key
511 auth_token = user_admin.api_key
500 assert auth_token
512 assert auth_token
501
513
502 with fixture.anon_access(False):
514 with fixture.anon_access(False):
503 # webtest uses linter to check if response is bytes,
515 # webtest uses linter to check if response is bytes,
504 # and we use memoryview here as a wrapper, quick turn-off
516 # and we use memoryview here as a wrapper, quick turn-off
505 self.app.lint = False
517 self.app.lint = False
506 self.app.get(
518 self.app.get(
507 route_path('repo_commit_raw',
519 route_path('repo_commit_raw',
508 repo_name=HG_REPO, commit_id='tip',
520 repo_name=HG_REPO, commit_id='tip',
509 params=dict(api_key=auth_token)),
521 params=dict(api_key=auth_token)),
510 status=code)
522 status=code)
511
523
512 @pytest.mark.parametrize("test_name, auth_token, code", [
524 @pytest.mark.parametrize("test_name, auth_token, code", [
513 ('proper_auth_token', None, 200),
525 ('proper_auth_token', None, 200),
514 ('wrong_auth_token', '123456', 302),
526 ('wrong_auth_token', '123456', 302),
515 ])
527 ])
516 def test_access_whitelisted_page_via_auth_token_bound_to_token(
528 def test_access_whitelisted_page_via_auth_token_bound_to_token(
517 self, test_name, auth_token, code, user_admin):
529 self, test_name, auth_token, code, user_admin):
518
530
519 expected_token = auth_token
531 expected_token = auth_token
520 if test_name == 'proper_auth_token':
532 if test_name == 'proper_auth_token':
521 auth_token = user_admin.api_key
533 auth_token = user_admin.api_key
522 expected_token = auth_token
534 expected_token = auth_token
523 assert auth_token
535 assert auth_token
524
536
525 whitelist = self._get_api_whitelist([
537 whitelist = self._get_api_whitelist([
526 'RepoCommitsView:repo_commit_raw@{}'.format(expected_token)])
538 'RepoCommitsView:repo_commit_raw@{}'.format(expected_token)])
527
539
528 with mock.patch.dict('rhodecode.CONFIG', whitelist):
540 with mock.patch.dict('rhodecode.CONFIG', whitelist):
529
541
530 with fixture.anon_access(False):
542 with fixture.anon_access(False):
531 # webtest uses linter to check if response is bytes,
543 # webtest uses linter to check if response is bytes,
532 # and we use memoryview here as a wrapper, quick turn-off
544 # and we use memoryview here as a wrapper, quick turn-off
533 self.app.lint = False
545 self.app.lint = False
534
546
535 self.app.get(
547 self.app.get(
536 route_path('repo_commit_raw',
548 route_path('repo_commit_raw',
537 repo_name=HG_REPO, commit_id='tip',
549 repo_name=HG_REPO, commit_id='tip',
538 params=dict(api_key=auth_token)),
550 params=dict(api_key=auth_token)),
539 status=code)
551 status=code)
540
552
541 def test_access_page_via_extra_auth_token(self):
553 def test_access_page_via_extra_auth_token(self):
542 whitelist = self._get_api_whitelist(whitelist_view)
554 whitelist = self._get_api_whitelist(whitelist_view)
543 with mock.patch.dict('rhodecode.CONFIG', whitelist):
555 with mock.patch.dict('rhodecode.CONFIG', whitelist):
544 assert whitelist_view == \
556 assert whitelist_view == \
545 whitelist['api_access_controllers_whitelist']
557 whitelist['api_access_controllers_whitelist']
546
558
547 new_auth_token = AuthTokenModel().create(
559 new_auth_token = AuthTokenModel().create(
548 TEST_USER_ADMIN_LOGIN, 'test')
560 TEST_USER_ADMIN_LOGIN, 'test')
549 Session().commit()
561 Session().commit()
550 with fixture.anon_access(False):
562 with fixture.anon_access(False):
551 # webtest uses linter to check if response is bytes,
563 # webtest uses linter to check if response is bytes,
552 # and we use memoryview here as a wrapper, quick turn-off
564 # and we use memoryview here as a wrapper, quick turn-off
553 self.app.lint = False
565 self.app.lint = False
554 self.app.get(
566 self.app.get(
555 route_path('repo_commit_raw',
567 route_path('repo_commit_raw',
556 repo_name=HG_REPO, commit_id='tip',
568 repo_name=HG_REPO, commit_id='tip',
557 params=dict(api_key=new_auth_token.api_key)),
569 params=dict(api_key=new_auth_token.api_key)),
558 status=200)
570 status=200)
559
571
560 def test_access_page_via_expired_auth_token(self):
572 def test_access_page_via_expired_auth_token(self):
561 whitelist = self._get_api_whitelist(whitelist_view)
573 whitelist = self._get_api_whitelist(whitelist_view)
562 with mock.patch.dict('rhodecode.CONFIG', whitelist):
574 with mock.patch.dict('rhodecode.CONFIG', whitelist):
563 assert whitelist_view == \
575 assert whitelist_view == \
564 whitelist['api_access_controllers_whitelist']
576 whitelist['api_access_controllers_whitelist']
565
577
566 new_auth_token = AuthTokenModel().create(
578 new_auth_token = AuthTokenModel().create(
567 TEST_USER_ADMIN_LOGIN, 'test')
579 TEST_USER_ADMIN_LOGIN, 'test')
568 Session().commit()
580 Session().commit()
569 # patch the api key and make it expired
581 # patch the api key and make it expired
570 new_auth_token.expires = 0
582 new_auth_token.expires = 0
571 Session().add(new_auth_token)
583 Session().add(new_auth_token)
572 Session().commit()
584 Session().commit()
573 with fixture.anon_access(False):
585 with fixture.anon_access(False):
574 # webtest uses linter to check if response is bytes,
586 # webtest uses linter to check if response is bytes,
575 # and we use memoryview here as a wrapper, quick turn-off
587 # and we use memoryview here as a wrapper, quick turn-off
576 self.app.lint = False
588 self.app.lint = False
577 self.app.get(
589 self.app.get(
578 route_path('repo_commit_raw',
590 route_path('repo_commit_raw',
579 repo_name=HG_REPO, commit_id='tip',
591 repo_name=HG_REPO, commit_id='tip',
580 params=dict(api_key=new_auth_token.api_key)),
592 params=dict(api_key=new_auth_token.api_key)),
581 status=302)
593 status=302)
@@ -1,469 +1,469 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import time
19 import time
20 import collections
20 import collections
21 import datetime
21 import datetime
22 import formencode
22 import formencode
23 import formencode.htmlfill
23 import formencode.htmlfill
24 import logging
24 import logging
25 import urllib.parse
25 import urllib.parse
26 import requests
26 import requests
27
27
28 from pyramid.httpexceptions import HTTPFound
28 from pyramid.httpexceptions import HTTPFound
29
29
30
30
31 from rhodecode.apps._base import BaseAppView
31 from rhodecode.apps._base import BaseAppView
32 from rhodecode.authentication.base import authenticate, HTTP_TYPE
32 from rhodecode.authentication.base import authenticate, HTTP_TYPE
33 from rhodecode.authentication.plugins import auth_rhodecode
33 from rhodecode.authentication.plugins import auth_rhodecode
34 from rhodecode.events import UserRegistered, trigger
34 from rhodecode.events import UserRegistered, trigger
35 from rhodecode.lib import helpers as h
35 from rhodecode.lib import helpers as h
36 from rhodecode.lib import audit_logger
36 from rhodecode.lib import audit_logger
37 from rhodecode.lib.auth import (
37 from rhodecode.lib.auth import (
38 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
38 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
39 from rhodecode.lib.base import get_ip_addr
39 from rhodecode.lib.base import get_ip_addr
40 from rhodecode.lib.exceptions import UserCreationError
40 from rhodecode.lib.exceptions import UserCreationError
41 from rhodecode.lib.utils2 import safe_str
41 from rhodecode.lib.utils2 import safe_str
42 from rhodecode.model.db import User, UserApiKeys
42 from rhodecode.model.db import User, UserApiKeys
43 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
43 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
44 from rhodecode.model.meta import Session
44 from rhodecode.model.meta import Session
45 from rhodecode.model.auth_token import AuthTokenModel
45 from rhodecode.model.auth_token import AuthTokenModel
46 from rhodecode.model.settings import SettingsModel
46 from rhodecode.model.settings import SettingsModel
47 from rhodecode.model.user import UserModel
47 from rhodecode.model.user import UserModel
48 from rhodecode.translation import _
48 from rhodecode.translation import _
49
49
50
50
51 log = logging.getLogger(__name__)
51 log = logging.getLogger(__name__)
52
52
53 CaptchaData = collections.namedtuple(
53 CaptchaData = collections.namedtuple(
54 'CaptchaData', 'active, private_key, public_key')
54 'CaptchaData', 'active, private_key, public_key')
55
55
56
56
57 def store_user_in_session(session, username, remember=False):
57 def store_user_in_session(session, user_identifier, remember=False):
58 user = User.get_by_username(username, case_insensitive=True)
58 user = User.get_by_username_or_primary_email(user_identifier)
59 auth_user = AuthUser(user.user_id)
59 auth_user = AuthUser(user.user_id)
60 auth_user.set_authenticated()
60 auth_user.set_authenticated()
61 cs = auth_user.get_cookie_store()
61 cs = auth_user.get_cookie_store()
62 session['rhodecode_user'] = cs
62 session['rhodecode_user'] = cs
63 user.update_lastlogin()
63 user.update_lastlogin()
64 Session().commit()
64 Session().commit()
65
65
66 # If they want to be remembered, update the cookie
66 # If they want to be remembered, update the cookie
67 if remember:
67 if remember:
68 _year = (datetime.datetime.now() +
68 _year = (datetime.datetime.now() +
69 datetime.timedelta(seconds=60 * 60 * 24 * 365))
69 datetime.timedelta(seconds=60 * 60 * 24 * 365))
70 session._set_cookie_expires(_year)
70 session._set_cookie_expires(_year)
71
71
72 session.save()
72 session.save()
73
73
74 safe_cs = cs.copy()
74 safe_cs = cs.copy()
75 safe_cs['password'] = '****'
75 safe_cs['password'] = '****'
76 log.info('user %s is now authenticated and stored in '
76 log.info('user %s is now authenticated and stored in '
77 'session, session attrs %s', username, safe_cs)
77 'session, session attrs %s', user_identifier, safe_cs)
78
78
79 # dumps session attrs back to cookie
79 # dumps session attrs back to cookie
80 session._update_cookie_out()
80 session._update_cookie_out()
81 # we set new cookie
81 # we set new cookie
82 headers = None
82 headers = None
83 if session.request['set_cookie']:
83 if session.request['set_cookie']:
84 # send set-cookie headers back to response to update cookie
84 # send set-cookie headers back to response to update cookie
85 headers = [('Set-Cookie', session.request['cookie_out'])]
85 headers = [('Set-Cookie', session.request['cookie_out'])]
86 return headers
86 return headers
87
87
88
88
89 def get_came_from(request):
89 def get_came_from(request):
90 came_from = safe_str(request.GET.get('came_from', ''))
90 came_from = safe_str(request.GET.get('came_from', ''))
91 parsed = urllib.parse.urlparse(came_from)
91 parsed = urllib.parse.urlparse(came_from)
92
92
93 allowed_schemes = ['http', 'https']
93 allowed_schemes = ['http', 'https']
94 default_came_from = h.route_path('home')
94 default_came_from = h.route_path('home')
95 if parsed.scheme and parsed.scheme not in allowed_schemes:
95 if parsed.scheme and parsed.scheme not in allowed_schemes:
96 log.error('Suspicious URL scheme detected %s for url %s',
96 log.error('Suspicious URL scheme detected %s for url %s',
97 parsed.scheme, parsed)
97 parsed.scheme, parsed)
98 came_from = default_came_from
98 came_from = default_came_from
99 elif parsed.netloc and request.host != parsed.netloc:
99 elif parsed.netloc and request.host != parsed.netloc:
100 log.error('Suspicious NETLOC detected %s for url %s server url '
100 log.error('Suspicious NETLOC detected %s for url %s server url '
101 'is: %s', parsed.netloc, parsed, request.host)
101 'is: %s', parsed.netloc, parsed, request.host)
102 came_from = default_came_from
102 came_from = default_came_from
103 elif any(bad_char in came_from for bad_char in ('\r', '\n')):
103 elif any(bad_char in came_from for bad_char in ('\r', '\n')):
104 log.error('Header injection detected `%s` for url %s server url ',
104 log.error('Header injection detected `%s` for url %s server url ',
105 parsed.path, parsed)
105 parsed.path, parsed)
106 came_from = default_came_from
106 came_from = default_came_from
107
107
108 return came_from or default_came_from
108 return came_from or default_came_from
109
109
110
110
111 class LoginView(BaseAppView):
111 class LoginView(BaseAppView):
112
112
113 def load_default_context(self):
113 def load_default_context(self):
114 c = self._get_local_tmpl_context()
114 c = self._get_local_tmpl_context()
115 c.came_from = get_came_from(self.request)
115 c.came_from = get_came_from(self.request)
116 return c
116 return c
117
117
118 def _get_captcha_data(self):
118 def _get_captcha_data(self):
119 settings = SettingsModel().get_all_settings()
119 settings = SettingsModel().get_all_settings()
120 private_key = settings.get('rhodecode_captcha_private_key')
120 private_key = settings.get('rhodecode_captcha_private_key')
121 public_key = settings.get('rhodecode_captcha_public_key')
121 public_key = settings.get('rhodecode_captcha_public_key')
122 active = bool(private_key)
122 active = bool(private_key)
123 return CaptchaData(
123 return CaptchaData(
124 active=active, private_key=private_key, public_key=public_key)
124 active=active, private_key=private_key, public_key=public_key)
125
125
126 def validate_captcha(self, private_key):
126 def validate_captcha(self, private_key):
127
127
128 captcha_rs = self.request.POST.get('g-recaptcha-response')
128 captcha_rs = self.request.POST.get('g-recaptcha-response')
129 url = "https://www.google.com/recaptcha/api/siteverify"
129 url = "https://www.google.com/recaptcha/api/siteverify"
130 params = {
130 params = {
131 'secret': private_key,
131 'secret': private_key,
132 'response': captcha_rs,
132 'response': captcha_rs,
133 'remoteip': get_ip_addr(self.request.environ)
133 'remoteip': get_ip_addr(self.request.environ)
134 }
134 }
135 verify_rs = requests.get(url, params=params, verify=True, timeout=60)
135 verify_rs = requests.get(url, params=params, verify=True, timeout=60)
136 verify_rs = verify_rs.json()
136 verify_rs = verify_rs.json()
137 captcha_status = verify_rs.get('success', False)
137 captcha_status = verify_rs.get('success', False)
138 captcha_errors = verify_rs.get('error-codes', [])
138 captcha_errors = verify_rs.get('error-codes', [])
139 if not isinstance(captcha_errors, list):
139 if not isinstance(captcha_errors, list):
140 captcha_errors = [captcha_errors]
140 captcha_errors = [captcha_errors]
141 captcha_errors = ', '.join(captcha_errors)
141 captcha_errors = ', '.join(captcha_errors)
142 captcha_message = ''
142 captcha_message = ''
143 if captcha_status is False:
143 if captcha_status is False:
144 captcha_message = "Bad captcha. Errors: {}".format(
144 captcha_message = "Bad captcha. Errors: {}".format(
145 captcha_errors)
145 captcha_errors)
146
146
147 return captcha_status, captcha_message
147 return captcha_status, captcha_message
148
148
149 def login(self):
149 def login(self):
150 c = self.load_default_context()
150 c = self.load_default_context()
151 auth_user = self._rhodecode_user
151 auth_user = self._rhodecode_user
152
152
153 # redirect if already logged in
153 # redirect if already logged in
154 if (auth_user.is_authenticated and
154 if (auth_user.is_authenticated and
155 not auth_user.is_default and auth_user.ip_allowed):
155 not auth_user.is_default and auth_user.ip_allowed):
156 raise HTTPFound(c.came_from)
156 raise HTTPFound(c.came_from)
157
157
158 # check if we use headers plugin, and try to login using it.
158 # check if we use headers plugin, and try to login using it.
159 try:
159 try:
160 log.debug('Running PRE-AUTH for headers based authentication')
160 log.debug('Running PRE-AUTH for headers based authentication')
161 auth_info = authenticate(
161 auth_info = authenticate(
162 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
162 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
163 if auth_info:
163 if auth_info:
164 headers = store_user_in_session(
164 headers = store_user_in_session(
165 self.session, auth_info.get('username'))
165 self.session, auth_info.get('username'))
166 raise HTTPFound(c.came_from, headers=headers)
166 raise HTTPFound(c.came_from, headers=headers)
167 except UserCreationError as e:
167 except UserCreationError as e:
168 log.error(e)
168 log.error(e)
169 h.flash(e, category='error')
169 h.flash(e, category='error')
170
170
171 return self._get_template_context(c)
171 return self._get_template_context(c)
172
172
173 def login_post(self):
173 def login_post(self):
174 c = self.load_default_context()
174 c = self.load_default_context()
175
175
176 login_form = LoginForm(self.request.translate)()
176 login_form = LoginForm(self.request.translate)()
177
177
178 try:
178 try:
179 self.session.invalidate()
179 self.session.invalidate()
180 form_result = login_form.to_python(self.request.POST)
180 form_result = login_form.to_python(self.request.POST)
181 # form checks for username/password, now we're authenticated
181 # form checks for username/password, now we're authenticated
182 headers = store_user_in_session(
182 headers = store_user_in_session(
183 self.session,
183 self.session,
184 username=form_result['username'],
184 user_identifier=form_result['username'],
185 remember=form_result['remember'])
185 remember=form_result['remember'])
186 log.debug('Redirecting to "%s" after login.', c.came_from)
186 log.debug('Redirecting to "%s" after login.', c.came_from)
187
187
188 audit_user = audit_logger.UserWrap(
188 audit_user = audit_logger.UserWrap(
189 username=self.request.POST.get('username'),
189 username=self.request.POST.get('username'),
190 ip_addr=self.request.remote_addr)
190 ip_addr=self.request.remote_addr)
191 action_data = {'user_agent': self.request.user_agent}
191 action_data = {'user_agent': self.request.user_agent}
192 audit_logger.store_web(
192 audit_logger.store_web(
193 'user.login.success', action_data=action_data,
193 'user.login.success', action_data=action_data,
194 user=audit_user, commit=True)
194 user=audit_user, commit=True)
195
195
196 raise HTTPFound(c.came_from, headers=headers)
196 raise HTTPFound(c.came_from, headers=headers)
197 except formencode.Invalid as errors:
197 except formencode.Invalid as errors:
198 defaults = errors.value
198 defaults = errors.value
199 # remove password from filling in form again
199 # remove password from filling in form again
200 defaults.pop('password', None)
200 defaults.pop('password', None)
201 render_ctx = {
201 render_ctx = {
202 'errors': errors.error_dict,
202 'errors': errors.error_dict,
203 'defaults': defaults,
203 'defaults': defaults,
204 }
204 }
205
205
206 audit_user = audit_logger.UserWrap(
206 audit_user = audit_logger.UserWrap(
207 username=self.request.POST.get('username'),
207 username=self.request.POST.get('username'),
208 ip_addr=self.request.remote_addr)
208 ip_addr=self.request.remote_addr)
209 action_data = {'user_agent': self.request.user_agent}
209 action_data = {'user_agent': self.request.user_agent}
210 audit_logger.store_web(
210 audit_logger.store_web(
211 'user.login.failure', action_data=action_data,
211 'user.login.failure', action_data=action_data,
212 user=audit_user, commit=True)
212 user=audit_user, commit=True)
213 return self._get_template_context(c, **render_ctx)
213 return self._get_template_context(c, **render_ctx)
214
214
215 except UserCreationError as e:
215 except UserCreationError as e:
216 # headers auth or other auth functions that create users on
216 # headers auth or other auth functions that create users on
217 # the fly can throw this exception signaling that there's issue
217 # the fly can throw this exception signaling that there's issue
218 # with user creation, explanation should be provided in
218 # with user creation, explanation should be provided in
219 # Exception itself
219 # Exception itself
220 h.flash(e, category='error')
220 h.flash(e, category='error')
221 return self._get_template_context(c)
221 return self._get_template_context(c)
222
222
223 @CSRFRequired()
223 @CSRFRequired()
224 def logout(self):
224 def logout(self):
225 auth_user = self._rhodecode_user
225 auth_user = self._rhodecode_user
226 log.info('Deleting session for user: `%s`', auth_user)
226 log.info('Deleting session for user: `%s`', auth_user)
227
227
228 action_data = {'user_agent': self.request.user_agent}
228 action_data = {'user_agent': self.request.user_agent}
229 audit_logger.store_web(
229 audit_logger.store_web(
230 'user.logout', action_data=action_data,
230 'user.logout', action_data=action_data,
231 user=auth_user, commit=True)
231 user=auth_user, commit=True)
232 self.session.delete()
232 self.session.delete()
233 return HTTPFound(h.route_path('home'))
233 return HTTPFound(h.route_path('home'))
234
234
235 @HasPermissionAnyDecorator(
235 @HasPermissionAnyDecorator(
236 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
236 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
237 def register(self, defaults=None, errors=None):
237 def register(self, defaults=None, errors=None):
238 c = self.load_default_context()
238 c = self.load_default_context()
239 defaults = defaults or {}
239 defaults = defaults or {}
240 errors = errors or {}
240 errors = errors or {}
241
241
242 settings = SettingsModel().get_all_settings()
242 settings = SettingsModel().get_all_settings()
243 register_message = settings.get('rhodecode_register_message') or ''
243 register_message = settings.get('rhodecode_register_message') or ''
244 captcha = self._get_captcha_data()
244 captcha = self._get_captcha_data()
245 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
245 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
246 .AuthUser().permissions['global']
246 .AuthUser().permissions['global']
247
247
248 render_ctx = self._get_template_context(c)
248 render_ctx = self._get_template_context(c)
249 render_ctx.update({
249 render_ctx.update({
250 'defaults': defaults,
250 'defaults': defaults,
251 'errors': errors,
251 'errors': errors,
252 'auto_active': auto_active,
252 'auto_active': auto_active,
253 'captcha_active': captcha.active,
253 'captcha_active': captcha.active,
254 'captcha_public_key': captcha.public_key,
254 'captcha_public_key': captcha.public_key,
255 'register_message': register_message,
255 'register_message': register_message,
256 })
256 })
257 return render_ctx
257 return render_ctx
258
258
259 @HasPermissionAnyDecorator(
259 @HasPermissionAnyDecorator(
260 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
260 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
261 def register_post(self):
261 def register_post(self):
262 from rhodecode.authentication.plugins import auth_rhodecode
262 from rhodecode.authentication.plugins import auth_rhodecode
263
263
264 self.load_default_context()
264 self.load_default_context()
265 captcha = self._get_captcha_data()
265 captcha = self._get_captcha_data()
266 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
266 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
267 .AuthUser().permissions['global']
267 .AuthUser().permissions['global']
268
268
269 extern_name = auth_rhodecode.RhodeCodeAuthPlugin.uid
269 extern_name = auth_rhodecode.RhodeCodeAuthPlugin.uid
270 extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
270 extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
271
271
272 register_form = RegisterForm(self.request.translate)()
272 register_form = RegisterForm(self.request.translate)()
273 try:
273 try:
274
274
275 form_result = register_form.to_python(self.request.POST)
275 form_result = register_form.to_python(self.request.POST)
276 form_result['active'] = auto_active
276 form_result['active'] = auto_active
277 external_identity = self.request.POST.get('external_identity')
277 external_identity = self.request.POST.get('external_identity')
278
278
279 if external_identity:
279 if external_identity:
280 extern_name = external_identity
280 extern_name = external_identity
281 extern_type = external_identity
281 extern_type = external_identity
282
282
283 if captcha.active:
283 if captcha.active:
284 captcha_status, captcha_message = self.validate_captcha(
284 captcha_status, captcha_message = self.validate_captcha(
285 captcha.private_key)
285 captcha.private_key)
286
286
287 if not captcha_status:
287 if not captcha_status:
288 _value = form_result
288 _value = form_result
289 _msg = _('Bad captcha')
289 _msg = _('Bad captcha')
290 error_dict = {'recaptcha_field': captcha_message}
290 error_dict = {'recaptcha_field': captcha_message}
291 raise formencode.Invalid(
291 raise formencode.Invalid(
292 _msg, _value, None, error_dict=error_dict)
292 _msg, _value, None, error_dict=error_dict)
293
293
294 new_user = UserModel().create_registration(
294 new_user = UserModel().create_registration(
295 form_result, extern_name=extern_name, extern_type=extern_type)
295 form_result, extern_name=extern_name, extern_type=extern_type)
296
296
297 action_data = {'data': new_user.get_api_data(),
297 action_data = {'data': new_user.get_api_data(),
298 'user_agent': self.request.user_agent}
298 'user_agent': self.request.user_agent}
299
299
300 if external_identity:
300 if external_identity:
301 action_data['external_identity'] = external_identity
301 action_data['external_identity'] = external_identity
302
302
303 audit_user = audit_logger.UserWrap(
303 audit_user = audit_logger.UserWrap(
304 username=new_user.username,
304 username=new_user.username,
305 user_id=new_user.user_id,
305 user_id=new_user.user_id,
306 ip_addr=self.request.remote_addr)
306 ip_addr=self.request.remote_addr)
307
307
308 audit_logger.store_web(
308 audit_logger.store_web(
309 'user.register', action_data=action_data,
309 'user.register', action_data=action_data,
310 user=audit_user)
310 user=audit_user)
311
311
312 event = UserRegistered(user=new_user, session=self.session)
312 event = UserRegistered(user=new_user, session=self.session)
313 trigger(event)
313 trigger(event)
314 h.flash(
314 h.flash(
315 _('You have successfully registered with RhodeCode. You can log-in now.'),
315 _('You have successfully registered with RhodeCode. You can log-in now.'),
316 category='success')
316 category='success')
317 if external_identity:
317 if external_identity:
318 h.flash(
318 h.flash(
319 _('Please use the {identity} button to log-in').format(
319 _('Please use the {identity} button to log-in').format(
320 identity=external_identity),
320 identity=external_identity),
321 category='success')
321 category='success')
322 Session().commit()
322 Session().commit()
323
323
324 redirect_ro = self.request.route_path('login')
324 redirect_ro = self.request.route_path('login')
325 raise HTTPFound(redirect_ro)
325 raise HTTPFound(redirect_ro)
326
326
327 except formencode.Invalid as errors:
327 except formencode.Invalid as errors:
328 errors.value.pop('password', None)
328 errors.value.pop('password', None)
329 errors.value.pop('password_confirmation', None)
329 errors.value.pop('password_confirmation', None)
330 return self.register(
330 return self.register(
331 defaults=errors.value, errors=errors.error_dict)
331 defaults=errors.value, errors=errors.error_dict)
332
332
333 except UserCreationError as e:
333 except UserCreationError as e:
334 # container auth or other auth functions that create users on
334 # container auth or other auth functions that create users on
335 # the fly can throw this exception signaling that there's issue
335 # the fly can throw this exception signaling that there's issue
336 # with user creation, explanation should be provided in
336 # with user creation, explanation should be provided in
337 # Exception itself
337 # Exception itself
338 h.flash(e, category='error')
338 h.flash(e, category='error')
339 return self.register()
339 return self.register()
340
340
341 def password_reset(self):
341 def password_reset(self):
342 c = self.load_default_context()
342 c = self.load_default_context()
343 captcha = self._get_captcha_data()
343 captcha = self._get_captcha_data()
344
344
345 template_context = {
345 template_context = {
346 'captcha_active': captcha.active,
346 'captcha_active': captcha.active,
347 'captcha_public_key': captcha.public_key,
347 'captcha_public_key': captcha.public_key,
348 'defaults': {},
348 'defaults': {},
349 'errors': {},
349 'errors': {},
350 }
350 }
351
351
352 # always send implicit message to prevent from discovery of
352 # always send implicit message to prevent from discovery of
353 # matching emails
353 # matching emails
354 msg = _('If such email exists, a password reset link was sent to it.')
354 msg = _('If such email exists, a password reset link was sent to it.')
355
355
356 def default_response():
356 def default_response():
357 log.debug('faking response on invalid password reset')
357 log.debug('faking response on invalid password reset')
358 # make this take 2s, to prevent brute forcing.
358 # make this take 2s, to prevent brute forcing.
359 time.sleep(2)
359 time.sleep(2)
360 h.flash(msg, category='success')
360 h.flash(msg, category='success')
361 return HTTPFound(self.request.route_path('reset_password'))
361 return HTTPFound(self.request.route_path('reset_password'))
362
362
363 if self.request.POST:
363 if self.request.POST:
364 if h.HasPermissionAny('hg.password_reset.disabled')():
364 if h.HasPermissionAny('hg.password_reset.disabled')():
365 _email = self.request.POST.get('email', '')
365 _email = self.request.POST.get('email', '')
366 log.error('Failed attempt to reset password for `%s`.', _email)
366 log.error('Failed attempt to reset password for `%s`.', _email)
367 h.flash(_('Password reset has been disabled.'), category='error')
367 h.flash(_('Password reset has been disabled.'), category='error')
368 return HTTPFound(self.request.route_path('reset_password'))
368 return HTTPFound(self.request.route_path('reset_password'))
369
369
370 password_reset_form = PasswordResetForm(self.request.translate)()
370 password_reset_form = PasswordResetForm(self.request.translate)()
371 description = 'Generated token for password reset from {}'.format(
371 description = 'Generated token for password reset from {}'.format(
372 datetime.datetime.now().isoformat())
372 datetime.datetime.now().isoformat())
373
373
374 try:
374 try:
375 form_result = password_reset_form.to_python(
375 form_result = password_reset_form.to_python(
376 self.request.POST)
376 self.request.POST)
377 user_email = form_result['email']
377 user_email = form_result['email']
378
378
379 if captcha.active:
379 if captcha.active:
380 captcha_status, captcha_message = self.validate_captcha(
380 captcha_status, captcha_message = self.validate_captcha(
381 captcha.private_key)
381 captcha.private_key)
382
382
383 if not captcha_status:
383 if not captcha_status:
384 _value = form_result
384 _value = form_result
385 _msg = _('Bad captcha')
385 _msg = _('Bad captcha')
386 error_dict = {'recaptcha_field': captcha_message}
386 error_dict = {'recaptcha_field': captcha_message}
387 raise formencode.Invalid(
387 raise formencode.Invalid(
388 _msg, _value, None, error_dict=error_dict)
388 _msg, _value, None, error_dict=error_dict)
389
389
390 # Generate reset URL and send mail.
390 # Generate reset URL and send mail.
391 user = User.get_by_email(user_email)
391 user = User.get_by_email(user_email)
392
392
393 # only allow rhodecode based users to reset their password
393 # only allow rhodecode based users to reset their password
394 # external auth shouldn't allow password reset
394 # external auth shouldn't allow password reset
395 if user and user.extern_type != auth_rhodecode.RhodeCodeAuthPlugin.uid:
395 if user and user.extern_type != auth_rhodecode.RhodeCodeAuthPlugin.uid:
396 log.warning('User %s with external type `%s` tried a password reset. '
396 log.warning('User %s with external type `%s` tried a password reset. '
397 'This try was rejected', user, user.extern_type)
397 'This try was rejected', user, user.extern_type)
398 return default_response()
398 return default_response()
399
399
400 # generate password reset token that expires in 10 minutes
400 # generate password reset token that expires in 10 minutes
401 reset_token = UserModel().add_auth_token(
401 reset_token = UserModel().add_auth_token(
402 user=user, lifetime_minutes=10,
402 user=user, lifetime_minutes=10,
403 role=UserModel.auth_token_role.ROLE_PASSWORD_RESET,
403 role=UserModel.auth_token_role.ROLE_PASSWORD_RESET,
404 description=description)
404 description=description)
405 Session().commit()
405 Session().commit()
406
406
407 log.debug('Successfully created password recovery token')
407 log.debug('Successfully created password recovery token')
408 password_reset_url = self.request.route_url(
408 password_reset_url = self.request.route_url(
409 'reset_password_confirmation',
409 'reset_password_confirmation',
410 _query={'key': reset_token.api_key})
410 _query={'key': reset_token.api_key})
411 UserModel().reset_password_link(
411 UserModel().reset_password_link(
412 form_result, password_reset_url)
412 form_result, password_reset_url)
413
413
414 action_data = {'email': user_email,
414 action_data = {'email': user_email,
415 'user_agent': self.request.user_agent}
415 'user_agent': self.request.user_agent}
416 audit_logger.store_web(
416 audit_logger.store_web(
417 'user.password.reset_request', action_data=action_data,
417 'user.password.reset_request', action_data=action_data,
418 user=self._rhodecode_user, commit=True)
418 user=self._rhodecode_user, commit=True)
419
419
420 return default_response()
420 return default_response()
421
421
422 except formencode.Invalid as errors:
422 except formencode.Invalid as errors:
423 template_context.update({
423 template_context.update({
424 'defaults': errors.value,
424 'defaults': errors.value,
425 'errors': errors.error_dict,
425 'errors': errors.error_dict,
426 })
426 })
427 if not self.request.POST.get('email'):
427 if not self.request.POST.get('email'):
428 # case of empty email, we want to report that
428 # case of empty email, we want to report that
429 return self._get_template_context(c, **template_context)
429 return self._get_template_context(c, **template_context)
430
430
431 if 'recaptcha_field' in errors.error_dict:
431 if 'recaptcha_field' in errors.error_dict:
432 # case of failed captcha
432 # case of failed captcha
433 return self._get_template_context(c, **template_context)
433 return self._get_template_context(c, **template_context)
434
434
435 return default_response()
435 return default_response()
436
436
437 return self._get_template_context(c, **template_context)
437 return self._get_template_context(c, **template_context)
438
438
439 def password_reset_confirmation(self):
439 def password_reset_confirmation(self):
440 self.load_default_context()
440 self.load_default_context()
441 if self.request.GET and self.request.GET.get('key'):
441 if self.request.GET and self.request.GET.get('key'):
442 # make this take 2s, to prevent brute forcing.
442 # make this take 2s, to prevent brute forcing.
443 time.sleep(2)
443 time.sleep(2)
444
444
445 token = AuthTokenModel().get_auth_token(
445 token = AuthTokenModel().get_auth_token(
446 self.request.GET.get('key'))
446 self.request.GET.get('key'))
447
447
448 # verify token is the correct role
448 # verify token is the correct role
449 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
449 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
450 log.debug('Got token with role:%s expected is %s',
450 log.debug('Got token with role:%s expected is %s',
451 getattr(token, 'role', 'EMPTY_TOKEN'),
451 getattr(token, 'role', 'EMPTY_TOKEN'),
452 UserApiKeys.ROLE_PASSWORD_RESET)
452 UserApiKeys.ROLE_PASSWORD_RESET)
453 h.flash(
453 h.flash(
454 _('Given reset token is invalid'), category='error')
454 _('Given reset token is invalid'), category='error')
455 return HTTPFound(self.request.route_path('reset_password'))
455 return HTTPFound(self.request.route_path('reset_password'))
456
456
457 try:
457 try:
458 owner = token.user
458 owner = token.user
459 data = {'email': owner.email, 'token': token.api_key}
459 data = {'email': owner.email, 'token': token.api_key}
460 UserModel().reset_password(data)
460 UserModel().reset_password(data)
461 h.flash(
461 h.flash(
462 _('Your password reset was successful, '
462 _('Your password reset was successful, '
463 'a new password has been sent to your email'),
463 'a new password has been sent to your email'),
464 category='success')
464 category='success')
465 except Exception as e:
465 except Exception as e:
466 log.error(e)
466 log.error(e)
467 return HTTPFound(self.request.route_path('reset_password'))
467 return HTTPFound(self.request.route_path('reset_password'))
468
468
469 return HTTPFound(self.request.route_path('login'))
469 return HTTPFound(self.request.route_path('login'))
@@ -1,825 +1,821 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 """
19 """
20 Authentication modules
20 Authentication modules
21 """
21 """
22 import socket
22 import socket
23 import string
23 import string
24 import colander
24 import colander
25 import copy
25 import copy
26 import logging
26 import logging
27 import time
27 import time
28 import traceback
28 import traceback
29 import warnings
29 import warnings
30 import functools
30 import functools
31
31
32 from pyramid.threadlocal import get_current_registry
32 from pyramid.threadlocal import get_current_registry
33
33
34 from rhodecode.authentication import AuthenticationPluginRegistry
34 from rhodecode.authentication import AuthenticationPluginRegistry
35 from rhodecode.authentication.interface import IAuthnPluginRegistry
35 from rhodecode.authentication.interface import IAuthnPluginRegistry
36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 from rhodecode.lib import rc_cache
37 from rhodecode.lib import rc_cache
38 from rhodecode.lib.statsd_client import StatsdClient
38 from rhodecode.lib.statsd_client import StatsdClient
39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
40 from rhodecode.lib.str_utils import safe_bytes
40 from rhodecode.lib.str_utils import safe_bytes
41 from rhodecode.lib.utils2 import safe_int, safe_str
41 from rhodecode.lib.utils2 import safe_int, safe_str
42 from rhodecode.lib.exceptions import (LdapConnectionError, LdapUsernameError, LdapPasswordError)
42 from rhodecode.lib.exceptions import (LdapConnectionError, LdapUsernameError, LdapPasswordError)
43 from rhodecode.model.db import User
43 from rhodecode.model.db import User
44 from rhodecode.model.meta import Session
44 from rhodecode.model.meta import Session
45 from rhodecode.model.settings import SettingsModel
45 from rhodecode.model.settings import SettingsModel
46 from rhodecode.model.user import UserModel
46 from rhodecode.model.user import UserModel
47 from rhodecode.model.user_group import UserGroupModel
47 from rhodecode.model.user_group import UserGroupModel
48
48
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52 # auth types that authenticate() function can receive
52 # auth types that authenticate() function can receive
53 VCS_TYPE = 'vcs'
53 VCS_TYPE = 'vcs'
54 HTTP_TYPE = 'http'
54 HTTP_TYPE = 'http'
55
55
56 external_auth_session_key = 'rhodecode.external_auth'
56 external_auth_session_key = 'rhodecode.external_auth'
57
57
58
58
59 class hybrid_property(object):
59 class hybrid_property(object):
60 """
60 """
61 a property decorator that works both for instance and class
61 a property decorator that works both for instance and class
62 """
62 """
63 def __init__(self, fget, fset=None, fdel=None, expr=None):
63 def __init__(self, fget, fset=None, fdel=None, expr=None):
64 self.fget = fget
64 self.fget = fget
65 self.fset = fset
65 self.fset = fset
66 self.fdel = fdel
66 self.fdel = fdel
67 self.expr = expr or fget
67 self.expr = expr or fget
68 functools.update_wrapper(self, fget)
68 functools.update_wrapper(self, fget)
69
69
70 def __get__(self, instance, owner):
70 def __get__(self, instance, owner):
71 if instance is None:
71 if instance is None:
72 return self.expr(owner)
72 return self.expr(owner)
73 else:
73 else:
74 return self.fget(instance)
74 return self.fget(instance)
75
75
76 def __set__(self, instance, value):
76 def __set__(self, instance, value):
77 self.fset(instance, value)
77 self.fset(instance, value)
78
78
79 def __delete__(self, instance):
79 def __delete__(self, instance):
80 self.fdel(instance)
80 self.fdel(instance)
81
81
82
82
83 class LazyFormencode(object):
83 class LazyFormencode(object):
84 def __init__(self, formencode_obj, *args, **kwargs):
84 def __init__(self, formencode_obj, *args, **kwargs):
85 self.formencode_obj = formencode_obj
85 self.formencode_obj = formencode_obj
86 self.args = args
86 self.args = args
87 self.kwargs = kwargs
87 self.kwargs = kwargs
88
88
89 def __call__(self, *args, **kwargs):
89 def __call__(self, *args, **kwargs):
90 from inspect import isfunction
90 from inspect import isfunction
91 formencode_obj = self.formencode_obj
91 formencode_obj = self.formencode_obj
92 if isfunction(formencode_obj):
92 if isfunction(formencode_obj):
93 # case we wrap validators into functions
93 # case we wrap validators into functions
94 formencode_obj = self.formencode_obj(*args, **kwargs)
94 formencode_obj = self.formencode_obj(*args, **kwargs)
95 return formencode_obj(*self.args, **self.kwargs)
95 return formencode_obj(*self.args, **self.kwargs)
96
96
97
97
98 class RhodeCodeAuthPluginBase(object):
98 class RhodeCodeAuthPluginBase(object):
99 # UID is used to register plugin to the registry
99 # UID is used to register plugin to the registry
100 uid = None
100 uid = None
101
101
102 # cache the authentication request for N amount of seconds. Some kind
102 # cache the authentication request for N amount of seconds. Some kind
103 # of authentication methods are very heavy and it's very efficient to cache
103 # of authentication methods are very heavy and it's very efficient to cache
104 # the result of a call. If it's set to None (default) cache is off
104 # the result of a call. If it's set to None (default) cache is off
105 AUTH_CACHE_TTL = None
105 AUTH_CACHE_TTL = None
106 AUTH_CACHE = {}
106 AUTH_CACHE = {}
107
107
108 auth_func_attrs = {
108 auth_func_attrs = {
109 "username": "unique username",
109 "username": "unique username",
110 "firstname": "first name",
110 "firstname": "first name",
111 "lastname": "last name",
111 "lastname": "last name",
112 "email": "email address",
112 "email": "email address",
113 "groups": '["list", "of", "groups"]',
113 "groups": '["list", "of", "groups"]',
114 "user_group_sync":
114 "user_group_sync":
115 'True|False defines if returned user groups should be synced',
115 'True|False defines if returned user groups should be synced',
116 "extern_name": "name in external source of record",
116 "extern_name": "name in external source of record",
117 "extern_type": "type of external source of record",
117 "extern_type": "type of external source of record",
118 "admin": 'True|False defines if user should be RhodeCode super admin',
118 "admin": 'True|False defines if user should be RhodeCode super admin',
119 "active":
119 "active":
120 'True|False defines active state of user internally for RhodeCode',
120 'True|False defines active state of user internally for RhodeCode',
121 "active_from_extern":
121 "active_from_extern":
122 "True|False|None, active state from the external auth, "
122 "True|False|None, active state from the external auth, "
123 "None means use definition from RhodeCode extern_type active value"
123 "None means use definition from RhodeCode extern_type active value"
124
124
125 }
125 }
126 # set on authenticate() method and via set_auth_type func.
126 # set on authenticate() method and via set_auth_type func.
127 auth_type = None
127 auth_type = None
128
128
129 # set on authenticate() method and via set_calling_scope_repo, this is a
129 # set on authenticate() method and via set_calling_scope_repo, this is a
130 # calling scope repository when doing authentication most likely on VCS
130 # calling scope repository when doing authentication most likely on VCS
131 # operations
131 # operations
132 acl_repo_name = None
132 acl_repo_name = None
133
133
134 # List of setting names to store encrypted. Plugins may override this list
134 # List of setting names to store encrypted. Plugins may override this list
135 # to store settings encrypted.
135 # to store settings encrypted.
136 _settings_encrypted = []
136 _settings_encrypted = []
137
137
138 # Mapping of python to DB settings model types. Plugins may override or
138 # Mapping of python to DB settings model types. Plugins may override or
139 # extend this mapping.
139 # extend this mapping.
140 _settings_type_map = {
140 _settings_type_map = {
141 colander.String: 'unicode',
141 colander.String: 'unicode',
142 colander.Integer: 'int',
142 colander.Integer: 'int',
143 colander.Boolean: 'bool',
143 colander.Boolean: 'bool',
144 colander.List: 'list',
144 colander.List: 'list',
145 }
145 }
146
146
147 # list of keys in settings that are unsafe to be logged, should be passwords
147 # list of keys in settings that are unsafe to be logged, should be passwords
148 # or other crucial credentials
148 # or other crucial credentials
149 _settings_unsafe_keys = []
149 _settings_unsafe_keys = []
150
150
151 def __init__(self, plugin_id):
151 def __init__(self, plugin_id):
152 self._plugin_id = plugin_id
152 self._plugin_id = plugin_id
153
153
154 def __str__(self):
154 def __str__(self):
155 return self.get_id()
155 return self.get_id()
156
156
157 def _get_setting_full_name(self, name):
157 def _get_setting_full_name(self, name):
158 """
158 """
159 Return the full setting name used for storing values in the database.
159 Return the full setting name used for storing values in the database.
160 """
160 """
161 # TODO: johbo: Using the name here is problematic. It would be good to
161 # TODO: johbo: Using the name here is problematic. It would be good to
162 # introduce either new models in the database to hold Plugin and
162 # introduce either new models in the database to hold Plugin and
163 # PluginSetting or to use the plugin id here.
163 # PluginSetting or to use the plugin id here.
164 return f'auth_{self.name}_{name}'
164 return f'auth_{self.name}_{name}'
165
165
166 def _get_setting_type(self, name):
166 def _get_setting_type(self, name):
167 """
167 """
168 Return the type of a setting. This type is defined by the SettingsModel
168 Return the type of a setting. This type is defined by the SettingsModel
169 and determines how the setting is stored in DB. Optionally the suffix
169 and determines how the setting is stored in DB. Optionally the suffix
170 `.encrypted` is appended to instruct SettingsModel to store it
170 `.encrypted` is appended to instruct SettingsModel to store it
171 encrypted.
171 encrypted.
172 """
172 """
173 schema_node = self.get_settings_schema().get(name)
173 schema_node = self.get_settings_schema().get(name)
174 db_type = self._settings_type_map.get(
174 db_type = self._settings_type_map.get(
175 type(schema_node.typ), 'unicode')
175 type(schema_node.typ), 'unicode')
176 if name in self._settings_encrypted:
176 if name in self._settings_encrypted:
177 db_type = f'{db_type}.encrypted'
177 db_type = f'{db_type}.encrypted'
178 return db_type
178 return db_type
179
179
180 @classmethod
180 @classmethod
181 def docs(cls):
181 def docs(cls):
182 """
182 """
183 Defines documentation url which helps with plugin setup
183 Defines documentation url which helps with plugin setup
184 """
184 """
185 return ''
185 return ''
186
186
187 @classmethod
187 @classmethod
188 def icon(cls):
188 def icon(cls):
189 """
189 """
190 Defines ICON in SVG format for authentication method
190 Defines ICON in SVG format for authentication method
191 """
191 """
192 return ''
192 return ''
193
193
194 def is_enabled(self):
194 def is_enabled(self):
195 """
195 """
196 Returns true if this plugin is enabled. An enabled plugin can be
196 Returns true if this plugin is enabled. An enabled plugin can be
197 configured in the admin interface but it is not consulted during
197 configured in the admin interface but it is not consulted during
198 authentication.
198 authentication.
199 """
199 """
200 auth_plugins = SettingsModel().get_auth_plugins()
200 auth_plugins = SettingsModel().get_auth_plugins()
201 return self.get_id() in auth_plugins
201 return self.get_id() in auth_plugins
202
202
203 def is_active(self, plugin_cached_settings=None):
203 def is_active(self, plugin_cached_settings=None):
204 """
204 """
205 Returns true if the plugin is activated. An activated plugin is
205 Returns true if the plugin is activated. An activated plugin is
206 consulted during authentication, assumed it is also enabled.
206 consulted during authentication, assumed it is also enabled.
207 """
207 """
208 return self.get_setting_by_name(
208 return self.get_setting_by_name(
209 'enabled', plugin_cached_settings=plugin_cached_settings)
209 'enabled', plugin_cached_settings=plugin_cached_settings)
210
210
211 def get_id(self):
211 def get_id(self):
212 """
212 """
213 Returns the plugin id.
213 Returns the plugin id.
214 """
214 """
215 return self._plugin_id
215 return self._plugin_id
216
216
217 def get_display_name(self, load_from_settings=False):
217 def get_display_name(self, load_from_settings=False):
218 """
218 """
219 Returns a translation string for displaying purposes.
219 Returns a translation string for displaying purposes.
220 if load_from_settings is set, plugin settings can override the display name
220 if load_from_settings is set, plugin settings can override the display name
221 """
221 """
222 raise NotImplementedError('Not implemented in base class')
222 raise NotImplementedError('Not implemented in base class')
223
223
224 def get_settings_schema(self):
224 def get_settings_schema(self):
225 """
225 """
226 Returns a colander schema, representing the plugin settings.
226 Returns a colander schema, representing the plugin settings.
227 """
227 """
228 return AuthnPluginSettingsSchemaBase()
228 return AuthnPluginSettingsSchemaBase()
229
229
230 def _propagate_settings(self, raw_settings):
230 def _propagate_settings(self, raw_settings):
231 settings = {}
231 settings = {}
232 for node in self.get_settings_schema():
232 for node in self.get_settings_schema():
233 settings[node.name] = self.get_setting_by_name(
233 settings[node.name] = self.get_setting_by_name(
234 node.name, plugin_cached_settings=raw_settings)
234 node.name, plugin_cached_settings=raw_settings)
235 return settings
235 return settings
236
236
237 def get_settings(self, use_cache=True):
237 def get_settings(self, use_cache=True):
238 """
238 """
239 Returns the plugin settings as dictionary.
239 Returns the plugin settings as dictionary.
240 """
240 """
241
241
242 raw_settings = SettingsModel().get_all_settings(cache=use_cache)
242 raw_settings = SettingsModel().get_all_settings(cache=use_cache)
243 settings = self._propagate_settings(raw_settings)
243 settings = self._propagate_settings(raw_settings)
244
244
245 return settings
245 return settings
246
246
247 def get_setting_by_name(self, name, default=None, plugin_cached_settings=None):
247 def get_setting_by_name(self, name, default=None, plugin_cached_settings=None):
248 """
248 """
249 Returns a plugin setting by name.
249 Returns a plugin setting by name.
250 """
250 """
251 full_name = f'rhodecode_{self._get_setting_full_name(name)}'
251 full_name = f'rhodecode_{self._get_setting_full_name(name)}'
252 if plugin_cached_settings:
252 if plugin_cached_settings:
253 plugin_settings = plugin_cached_settings
253 plugin_settings = plugin_cached_settings
254 else:
254 else:
255 plugin_settings = SettingsModel().get_all_settings()
255 plugin_settings = SettingsModel().get_all_settings()
256
256
257 if full_name in plugin_settings:
257 if full_name in plugin_settings:
258 return plugin_settings[full_name]
258 return plugin_settings[full_name]
259 else:
259 else:
260 return default
260 return default
261
261
262 def create_or_update_setting(self, name, value):
262 def create_or_update_setting(self, name, value):
263 """
263 """
264 Create or update a setting for this plugin in the persistent storage.
264 Create or update a setting for this plugin in the persistent storage.
265 """
265 """
266 full_name = self._get_setting_full_name(name)
266 full_name = self._get_setting_full_name(name)
267 type_ = self._get_setting_type(name)
267 type_ = self._get_setting_type(name)
268 db_setting = SettingsModel().create_or_update_setting(
268 db_setting = SettingsModel().create_or_update_setting(
269 full_name, value, type_)
269 full_name, value, type_)
270 return db_setting.app_settings_value
270 return db_setting.app_settings_value
271
271
272 def log_safe_settings(self, settings):
272 def log_safe_settings(self, settings):
273 """
273 """
274 returns a log safe representation of settings, without any secrets
274 returns a log safe representation of settings, without any secrets
275 """
275 """
276 settings_copy = copy.deepcopy(settings)
276 settings_copy = copy.deepcopy(settings)
277 for k in self._settings_unsafe_keys:
277 for k in self._settings_unsafe_keys:
278 if k in settings_copy:
278 if k in settings_copy:
279 del settings_copy[k]
279 del settings_copy[k]
280 return settings_copy
280 return settings_copy
281
281
282 @hybrid_property
282 @hybrid_property
283 def name(self):
283 def name(self):
284 """
284 """
285 Returns the name of this authentication plugin.
285 Returns the name of this authentication plugin.
286
286
287 :returns: string
287 :returns: string
288 """
288 """
289 raise NotImplementedError("Not implemented in base class")
289 raise NotImplementedError("Not implemented in base class")
290
290
291 def get_url_slug(self):
291 def get_url_slug(self):
292 """
292 """
293 Returns a slug which should be used when constructing URLs which refer
293 Returns a slug which should be used when constructing URLs which refer
294 to this plugin. By default it returns the plugin name. If the name is
294 to this plugin. By default it returns the plugin name. If the name is
295 not suitable for using it in an URL the plugin should override this
295 not suitable for using it in an URL the plugin should override this
296 method.
296 method.
297 """
297 """
298 return self.name
298 return self.name
299
299
300 @property
300 @property
301 def is_headers_auth(self):
301 def is_headers_auth(self):
302 """
302 """
303 Returns True if this authentication plugin uses HTTP headers as
303 Returns True if this authentication plugin uses HTTP headers as
304 authentication method.
304 authentication method.
305 """
305 """
306 return False
306 return False
307
307
308 @hybrid_property
308 @hybrid_property
309 def is_container_auth(self):
309 def is_container_auth(self):
310 """
310 """
311 Deprecated method that indicates if this authentication plugin uses
311 Deprecated method that indicates if this authentication plugin uses
312 HTTP headers as authentication method.
312 HTTP headers as authentication method.
313 """
313 """
314 warnings.warn(
314 warnings.warn(
315 'Use is_headers_auth instead.', category=DeprecationWarning)
315 'Use is_headers_auth instead.', category=DeprecationWarning)
316 return self.is_headers_auth
316 return self.is_headers_auth
317
317
318 @hybrid_property
318 @hybrid_property
319 def allows_creating_users(self):
319 def allows_creating_users(self):
320 """
320 """
321 Defines if Plugin allows users to be created on-the-fly when
321 Defines if Plugin allows users to be created on-the-fly when
322 authentication is called. Controls how external plugins should behave
322 authentication is called. Controls how external plugins should behave
323 in terms if they are allowed to create new users, or not. Base plugins
323 in terms if they are allowed to create new users, or not. Base plugins
324 should not be allowed to, but External ones should be !
324 should not be allowed to, but External ones should be !
325
325
326 :return: bool
326 :return: bool
327 """
327 """
328 return False
328 return False
329
329
330 def set_auth_type(self, auth_type):
330 def set_auth_type(self, auth_type):
331 self.auth_type = auth_type
331 self.auth_type = auth_type
332
332
333 def set_calling_scope_repo(self, acl_repo_name):
333 def set_calling_scope_repo(self, acl_repo_name):
334 self.acl_repo_name = acl_repo_name
334 self.acl_repo_name = acl_repo_name
335
335
336 def allows_authentication_from(
336 def allows_authentication_from(
337 self, user, allows_non_existing_user=True,
337 self, user, allows_non_existing_user=True,
338 allowed_auth_plugins=None, allowed_auth_sources=None):
338 allowed_auth_plugins=None, allowed_auth_sources=None):
339 """
339 """
340 Checks if this authentication module should accept a request for
340 Checks if this authentication module should accept a request for
341 the current user.
341 the current user.
342
342
343 :param user: user object fetched using plugin's get_user() method.
343 :param user: user object fetched using plugin's get_user() method.
344 :param allows_non_existing_user: if True, don't allow the
344 :param allows_non_existing_user: if True, don't allow the
345 user to be empty, meaning not existing in our database
345 user to be empty, meaning not existing in our database
346 :param allowed_auth_plugins: if provided, users extern_type will be
346 :param allowed_auth_plugins: if provided, users extern_type will be
347 checked against a list of provided extern types, which are plugin
347 checked against a list of provided extern types, which are plugin
348 auth_names in the end
348 auth_names in the end
349 :param allowed_auth_sources: authentication type allowed,
349 :param allowed_auth_sources: authentication type allowed,
350 `http` or `vcs` default is both.
350 `http` or `vcs` default is both.
351 defines if plugin will accept only http authentication vcs
351 defines if plugin will accept only http authentication vcs
352 authentication(git/hg) or both
352 authentication(git/hg) or both
353 :returns: boolean
353 :returns: boolean
354 """
354 """
355 if not user and not allows_non_existing_user:
355 if not user and not allows_non_existing_user:
356 log.debug('User is empty but plugin does not allow empty users,'
356 log.debug('User is empty but plugin does not allow empty users,'
357 'not allowed to authenticate')
357 'not allowed to authenticate')
358 return False
358 return False
359
359
360 expected_auth_plugins = allowed_auth_plugins or [self.name]
360 expected_auth_plugins = allowed_auth_plugins or [self.name]
361 if user and (user.extern_type and
361 if user and (user.extern_type and
362 user.extern_type not in expected_auth_plugins):
362 user.extern_type not in expected_auth_plugins):
363 log.debug(
363 log.debug(
364 'User `%s` is bound to `%s` auth type. Plugin allows only '
364 'User `%s` is bound to `%s` auth type. Plugin allows only '
365 '%s, skipping', user, user.extern_type, expected_auth_plugins)
365 '%s, skipping', user, user.extern_type, expected_auth_plugins)
366
366
367 return False
367 return False
368
368
369 # by default accept both
369 # by default accept both
370 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
370 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
371 if self.auth_type not in expected_auth_from:
371 if self.auth_type not in expected_auth_from:
372 log.debug('Current auth source is %s but plugin only allows %s',
372 log.debug('Current auth source is %s but plugin only allows %s',
373 self.auth_type, expected_auth_from)
373 self.auth_type, expected_auth_from)
374 return False
374 return False
375
375
376 return True
376 return True
377
377
378 def get_user(self, username=None, **kwargs):
378 def get_user(self, username=None, **kwargs):
379 """
379 """
380 Helper method for user fetching in plugins, by default it's using
380 Helper method for user fetching in plugins, by default it's using
381 simple fetch by username, but this method can be customized in plugins
381 simple fetch by username, but this method can be customized in plugins
382 eg. headers auth plugin to fetch user by environ params
382 eg. headers auth plugin to fetch user by environ params
383
383
384 :param username: username if given to fetch from database
384 :param username: username if given to fetch from database
385 :param kwargs: extra arguments needed for user fetching.
385 :param kwargs: extra arguments needed for user fetching.
386 """
386 """
387
387
388 user = None
388 user = None
389 log.debug(
389 log.debug(
390 'Trying to fetch user `%s` from RhodeCode database', username)
390 'Trying to fetch user `%s` from RhodeCode database', username)
391 if username:
391 if username:
392 user = User.get_by_username(username)
392 user = User.get_by_username_or_primary_email(username)
393 if not user:
394 log.debug('User not found, fallback to fetch user in '
395 'case insensitive mode')
396 user = User.get_by_username(username, case_insensitive=True)
397 else:
393 else:
398 log.debug('provided username:`%s` is empty skipping...', username)
394 log.debug('provided username:`%s` is empty skipping...', username)
399 if not user:
395 if not user:
400 log.debug('User `%s` not found in database', username)
396 log.debug('User `%s` not found in database', username)
401 else:
397 else:
402 log.debug('Got DB user:%s', user)
398 log.debug('Got DB user:%s', user)
403 return user
399 return user
404
400
405 def user_activation_state(self):
401 def user_activation_state(self):
406 """
402 """
407 Defines user activation state when creating new users
403 Defines user activation state when creating new users
408
404
409 :returns: boolean
405 :returns: boolean
410 """
406 """
411 raise NotImplementedError("Not implemented in base class")
407 raise NotImplementedError("Not implemented in base class")
412
408
413 def auth(self, userobj, username, passwd, settings, **kwargs):
409 def auth(self, userobj, username, passwd, settings, **kwargs):
414 """
410 """
415 Given a user object (which may be null), username, a plaintext
411 Given a user object (which may be null), username, a plaintext
416 password, and a settings object (containing all the keys needed as
412 password, and a settings object (containing all the keys needed as
417 listed in settings()), authenticate this user's login attempt.
413 listed in settings()), authenticate this user's login attempt.
418
414
419 Return None on failure. On success, return a dictionary of the form:
415 Return None on failure. On success, return a dictionary of the form:
420
416
421 see: RhodeCodeAuthPluginBase.auth_func_attrs
417 see: RhodeCodeAuthPluginBase.auth_func_attrs
422 This is later validated for correctness
418 This is later validated for correctness
423 """
419 """
424 raise NotImplementedError("not implemented in base class")
420 raise NotImplementedError("not implemented in base class")
425
421
426 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
422 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
427 """
423 """
428 Wrapper to call self.auth() that validates call on it
424 Wrapper to call self.auth() that validates call on it
429
425
430 :param userobj: userobj
426 :param userobj: userobj
431 :param username: username
427 :param username: username
432 :param passwd: plaintext password
428 :param passwd: plaintext password
433 :param settings: plugin settings
429 :param settings: plugin settings
434 """
430 """
435 auth = self.auth(userobj, username, passwd, settings, **kwargs)
431 auth = self.auth(userobj, username, passwd, settings, **kwargs)
436 if auth:
432 if auth:
437 auth['_plugin'] = self.name
433 auth['_plugin'] = self.name
438 auth['_ttl_cache'] = self.get_ttl_cache(settings)
434 auth['_ttl_cache'] = self.get_ttl_cache(settings)
439 # check if hash should be migrated ?
435 # check if hash should be migrated ?
440 new_hash = auth.get('_hash_migrate')
436 new_hash = auth.get('_hash_migrate')
441 if new_hash:
437 if new_hash:
442 # new_hash is a newly encrypted destination hash
438 # new_hash is a newly encrypted destination hash
443 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
439 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
444 if 'user_group_sync' not in auth:
440 if 'user_group_sync' not in auth:
445 auth['user_group_sync'] = False
441 auth['user_group_sync'] = False
446 return self._validate_auth_return(auth)
442 return self._validate_auth_return(auth)
447 return auth
443 return auth
448
444
449 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
445 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
450 new_hash_cypher = _RhodeCodeCryptoBCrypt()
446 new_hash_cypher = _RhodeCodeCryptoBCrypt()
451 # extra checks, so make sure new hash is correct.
447 # extra checks, so make sure new hash is correct.
452 password_as_bytes = safe_bytes(password)
448 password_as_bytes = safe_bytes(password)
453
449
454 if new_hash and new_hash_cypher.hash_check(password_as_bytes, new_hash):
450 if new_hash and new_hash_cypher.hash_check(password_as_bytes, new_hash):
455 cur_user = User.get_by_username(username)
451 cur_user = User.get_by_username(username)
456 cur_user.password = new_hash
452 cur_user.password = new_hash
457 Session().add(cur_user)
453 Session().add(cur_user)
458 Session().flush()
454 Session().flush()
459 log.info('Migrated user %s hash to bcrypt', cur_user)
455 log.info('Migrated user %s hash to bcrypt', cur_user)
460
456
461 def _validate_auth_return(self, ret):
457 def _validate_auth_return(self, ret):
462 if not isinstance(ret, dict):
458 if not isinstance(ret, dict):
463 raise Exception('returned value from auth must be a dict')
459 raise Exception('returned value from auth must be a dict')
464 for k in self.auth_func_attrs:
460 for k in self.auth_func_attrs:
465 if k not in ret:
461 if k not in ret:
466 raise Exception('Missing %s attribute from returned data' % k)
462 raise Exception('Missing %s attribute from returned data' % k)
467 return ret
463 return ret
468
464
469 def get_ttl_cache(self, settings=None):
465 def get_ttl_cache(self, settings=None):
470 plugin_settings = settings or self.get_settings()
466 plugin_settings = settings or self.get_settings()
471 # we set default to 30, we make a compromise here,
467 # we set default to 30, we make a compromise here,
472 # performance > security, mostly due to LDAP/SVN, majority
468 # performance > security, mostly due to LDAP/SVN, majority
473 # of users pick cache_ttl to be enabled
469 # of users pick cache_ttl to be enabled
474 from rhodecode.authentication import plugin_default_auth_ttl
470 from rhodecode.authentication import plugin_default_auth_ttl
475 cache_ttl = plugin_default_auth_ttl
471 cache_ttl = plugin_default_auth_ttl
476
472
477 if isinstance(self.AUTH_CACHE_TTL, int):
473 if isinstance(self.AUTH_CACHE_TTL, int):
478 # plugin cache set inside is more important than the settings value
474 # plugin cache set inside is more important than the settings value
479 cache_ttl = self.AUTH_CACHE_TTL
475 cache_ttl = self.AUTH_CACHE_TTL
480 elif 'cache_ttl' in plugin_settings:
476 elif 'cache_ttl' in plugin_settings:
481 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
477 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
482
478
483 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
479 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
484 return plugin_cache_active, cache_ttl
480 return plugin_cache_active, cache_ttl
485
481
486
482
487 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
483 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
488
484
489 @hybrid_property
485 @hybrid_property
490 def allows_creating_users(self):
486 def allows_creating_users(self):
491 return True
487 return True
492
488
493 def use_fake_password(self):
489 def use_fake_password(self):
494 """
490 """
495 Return a boolean that indicates whether or not we should set the user's
491 Return a boolean that indicates whether or not we should set the user's
496 password to a random value when it is authenticated by this plugin.
492 password to a random value when it is authenticated by this plugin.
497 If your plugin provides authentication, then you will generally
493 If your plugin provides authentication, then you will generally
498 want this.
494 want this.
499
495
500 :returns: boolean
496 :returns: boolean
501 """
497 """
502 raise NotImplementedError("Not implemented in base class")
498 raise NotImplementedError("Not implemented in base class")
503
499
504 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
500 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
505 # at this point _authenticate calls plugin's `auth()` function
501 # at this point _authenticate calls plugin's `auth()` function
506 auth = super()._authenticate(
502 auth = super()._authenticate(
507 userobj, username, passwd, settings, **kwargs)
503 userobj, username, passwd, settings, **kwargs)
508
504
509 if auth:
505 if auth:
510 # maybe plugin will clean the username ?
506 # maybe plugin will clean the username ?
511 # we should use the return value
507 # we should use the return value
512 username = auth['username']
508 username = auth['username']
513
509
514 # if external source tells us that user is not active, we should
510 # if external source tells us that user is not active, we should
515 # skip rest of the process. This can prevent from creating users in
511 # skip rest of the process. This can prevent from creating users in
516 # RhodeCode when using external authentication, but if it's
512 # RhodeCode when using external authentication, but if it's
517 # inactive user we shouldn't create that user anyway
513 # inactive user we shouldn't create that user anyway
518 if auth['active_from_extern'] is False:
514 if auth['active_from_extern'] is False:
519 log.warning(
515 log.warning(
520 "User %s authenticated against %s, but is inactive",
516 "User %s authenticated against %s, but is inactive",
521 username, self.__module__)
517 username, self.__module__)
522 return None
518 return None
523
519
524 cur_user = User.get_by_username(username, case_insensitive=True)
520 cur_user = User.get_by_username(username, case_insensitive=True)
525 is_user_existing = cur_user is not None
521 is_user_existing = cur_user is not None
526
522
527 if is_user_existing:
523 if is_user_existing:
528 log.debug('Syncing user `%s` from '
524 log.debug('Syncing user `%s` from '
529 '`%s` plugin', username, self.name)
525 '`%s` plugin', username, self.name)
530 else:
526 else:
531 log.debug('Creating non existing user `%s` from '
527 log.debug('Creating non existing user `%s` from '
532 '`%s` plugin', username, self.name)
528 '`%s` plugin', username, self.name)
533
529
534 if self.allows_creating_users:
530 if self.allows_creating_users:
535 log.debug('Plugin `%s` allows to '
531 log.debug('Plugin `%s` allows to '
536 'create new users', self.name)
532 'create new users', self.name)
537 else:
533 else:
538 log.debug('Plugin `%s` does not allow to '
534 log.debug('Plugin `%s` does not allow to '
539 'create new users', self.name)
535 'create new users', self.name)
540
536
541 user_parameters = {
537 user_parameters = {
542 'username': username,
538 'username': username,
543 'email': auth["email"],
539 'email': auth["email"],
544 'firstname': auth["firstname"],
540 'firstname': auth["firstname"],
545 'lastname': auth["lastname"],
541 'lastname': auth["lastname"],
546 'active': auth["active"],
542 'active': auth["active"],
547 'admin': auth["admin"],
543 'admin': auth["admin"],
548 'extern_name': auth["extern_name"],
544 'extern_name': auth["extern_name"],
549 'extern_type': self.name,
545 'extern_type': self.name,
550 'plugin': self,
546 'plugin': self,
551 'allow_to_create_user': self.allows_creating_users,
547 'allow_to_create_user': self.allows_creating_users,
552 }
548 }
553
549
554 if not is_user_existing:
550 if not is_user_existing:
555 if self.use_fake_password():
551 if self.use_fake_password():
556 # Randomize the PW because we don't need it, but don't want
552 # Randomize the PW because we don't need it, but don't want
557 # them blank either
553 # them blank either
558 passwd = PasswordGenerator().gen_password(length=16)
554 passwd = PasswordGenerator().gen_password(length=16)
559 user_parameters['password'] = passwd
555 user_parameters['password'] = passwd
560 else:
556 else:
561 # Since the password is required by create_or_update method of
557 # Since the password is required by create_or_update method of
562 # UserModel, we need to set it explicitly.
558 # UserModel, we need to set it explicitly.
563 # The create_or_update method is smart and recognises the
559 # The create_or_update method is smart and recognises the
564 # password hashes as well.
560 # password hashes as well.
565 user_parameters['password'] = cur_user.password
561 user_parameters['password'] = cur_user.password
566
562
567 # we either create or update users, we also pass the flag
563 # we either create or update users, we also pass the flag
568 # that controls if this method can actually do that.
564 # that controls if this method can actually do that.
569 # raises NotAllowedToCreateUserError if it cannot, and we try to.
565 # raises NotAllowedToCreateUserError if it cannot, and we try to.
570 user = UserModel().create_or_update(**user_parameters)
566 user = UserModel().create_or_update(**user_parameters)
571 Session().flush()
567 Session().flush()
572 # enforce user is just in given groups, all of them has to be ones
568 # enforce user is just in given groups, all of them has to be ones
573 # created from plugins. We store this info in _group_data JSON
569 # created from plugins. We store this info in _group_data JSON
574 # field
570 # field
575
571
576 if auth['user_group_sync']:
572 if auth['user_group_sync']:
577 try:
573 try:
578 groups = auth['groups'] or []
574 groups = auth['groups'] or []
579 log.debug(
575 log.debug(
580 'Performing user_group sync based on set `%s` '
576 'Performing user_group sync based on set `%s` '
581 'returned by `%s` plugin', groups, self.name)
577 'returned by `%s` plugin', groups, self.name)
582 UserGroupModel().enforce_groups(user, groups, self.name)
578 UserGroupModel().enforce_groups(user, groups, self.name)
583 except Exception:
579 except Exception:
584 # for any reason group syncing fails, we should
580 # for any reason group syncing fails, we should
585 # proceed with login
581 # proceed with login
586 log.error(traceback.format_exc())
582 log.error(traceback.format_exc())
587
583
588 Session().commit()
584 Session().commit()
589 return auth
585 return auth
590
586
591
587
592 class AuthLdapBase(object):
588 class AuthLdapBase(object):
593
589
594 @classmethod
590 @classmethod
595 def _build_servers(cls, ldap_server_type, ldap_server, port, use_resolver=True):
591 def _build_servers(cls, ldap_server_type, ldap_server, port, use_resolver=True):
596
592
597 def host_resolver(host, port, full_resolve=True):
593 def host_resolver(host, port, full_resolve=True):
598 """
594 """
599 Main work for this function is to prevent ldap connection issues,
595 Main work for this function is to prevent ldap connection issues,
600 and detect them early using a "greenified" sockets
596 and detect them early using a "greenified" sockets
601 """
597 """
602 host = host.strip()
598 host = host.strip()
603 if not full_resolve:
599 if not full_resolve:
604 return f'{host}:{port}'
600 return f'{host}:{port}'
605
601
606 log.debug('LDAP: Resolving IP for LDAP host `%s`', host)
602 log.debug('LDAP: Resolving IP for LDAP host `%s`', host)
607 try:
603 try:
608 ip = socket.gethostbyname(host)
604 ip = socket.gethostbyname(host)
609 log.debug('LDAP: Got LDAP host `%s` ip %s', host, ip)
605 log.debug('LDAP: Got LDAP host `%s` ip %s', host, ip)
610 except Exception:
606 except Exception:
611 raise LdapConnectionError(f'Failed to resolve host: `{host}`')
607 raise LdapConnectionError(f'Failed to resolve host: `{host}`')
612
608
613 log.debug('LDAP: Checking if IP %s is accessible', ip)
609 log.debug('LDAP: Checking if IP %s is accessible', ip)
614 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
610 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
615 try:
611 try:
616 s.connect((ip, int(port)))
612 s.connect((ip, int(port)))
617 s.shutdown(socket.SHUT_RD)
613 s.shutdown(socket.SHUT_RD)
618 log.debug('LDAP: connection to %s successful', ip)
614 log.debug('LDAP: connection to %s successful', ip)
619 except Exception:
615 except Exception:
620 raise LdapConnectionError(
616 raise LdapConnectionError(
621 f'Failed to connect to host: `{host}:{port}`')
617 f'Failed to connect to host: `{host}:{port}`')
622
618
623 return f'{host}:{port}'
619 return f'{host}:{port}'
624
620
625 if len(ldap_server) == 1:
621 if len(ldap_server) == 1:
626 # in case of single server use resolver to detect potential
622 # in case of single server use resolver to detect potential
627 # connection issues
623 # connection issues
628 full_resolve = True
624 full_resolve = True
629 else:
625 else:
630 full_resolve = False
626 full_resolve = False
631
627
632 return ', '.join(
628 return ', '.join(
633 ["{}://{}".format(
629 ["{}://{}".format(
634 ldap_server_type,
630 ldap_server_type,
635 host_resolver(host, port, full_resolve=use_resolver and full_resolve))
631 host_resolver(host, port, full_resolve=use_resolver and full_resolve))
636 for host in ldap_server])
632 for host in ldap_server])
637
633
638 @classmethod
634 @classmethod
639 def _get_server_list(cls, servers):
635 def _get_server_list(cls, servers):
640 return [s.strip() for s in servers.split(',')]
636 return [s.strip() for s in servers.split(',')]
641
637
642 @classmethod
638 @classmethod
643 def get_uid(cls, username, server_addresses):
639 def get_uid(cls, username, server_addresses):
644 uid = username
640 uid = username
645 for server_addr in server_addresses:
641 for server_addr in server_addresses:
646 uid = chop_at(username, "@%s" % server_addr)
642 uid = chop_at(username, "@%s" % server_addr)
647 return uid
643 return uid
648
644
649 @classmethod
645 @classmethod
650 def validate_username(cls, username):
646 def validate_username(cls, username):
651 if "," in username:
647 if "," in username:
652 raise LdapUsernameError(
648 raise LdapUsernameError(
653 f"invalid character `,` in username: `{username}`")
649 f"invalid character `,` in username: `{username}`")
654
650
655 @classmethod
651 @classmethod
656 def validate_password(cls, username, password):
652 def validate_password(cls, username, password):
657 if not password:
653 if not password:
658 msg = "Authenticating user %s with blank password not allowed"
654 msg = "Authenticating user %s with blank password not allowed"
659 log.warning(msg, username)
655 log.warning(msg, username)
660 raise LdapPasswordError(msg)
656 raise LdapPasswordError(msg)
661
657
662
658
663 def loadplugin(plugin_id):
659 def loadplugin(plugin_id):
664 """
660 """
665 Loads and returns an instantiated authentication plugin.
661 Loads and returns an instantiated authentication plugin.
666 Returns the RhodeCodeAuthPluginBase subclass on success,
662 Returns the RhodeCodeAuthPluginBase subclass on success,
667 or None on failure.
663 or None on failure.
668 """
664 """
669 # TODO: Disusing pyramids thread locals to retrieve the registry.
665 # TODO: Disusing pyramids thread locals to retrieve the registry.
670 authn_registry = get_authn_registry()
666 authn_registry = get_authn_registry()
671 plugin = authn_registry.get_plugin(plugin_id)
667 plugin = authn_registry.get_plugin(plugin_id)
672 if plugin is None:
668 if plugin is None:
673 log.error('Authentication plugin not found: "%s"', plugin_id)
669 log.error('Authentication plugin not found: "%s"', plugin_id)
674 return plugin
670 return plugin
675
671
676
672
677 def get_authn_registry(registry=None) -> AuthenticationPluginRegistry:
673 def get_authn_registry(registry=None) -> AuthenticationPluginRegistry:
678 registry = registry or get_current_registry()
674 registry = registry or get_current_registry()
679 authn_registry = registry.queryUtility(IAuthnPluginRegistry)
675 authn_registry = registry.queryUtility(IAuthnPluginRegistry)
680 return authn_registry
676 return authn_registry
681
677
682
678
683 def authenticate(username, password, environ=None, auth_type=None,
679 def authenticate(username, password, environ=None, auth_type=None,
684 skip_missing=False, registry=None, acl_repo_name=None):
680 skip_missing=False, registry=None, acl_repo_name=None):
685 """
681 """
686 Authentication function used for access control,
682 Authentication function used for access control,
687 It tries to authenticate based on enabled authentication modules.
683 It tries to authenticate based on enabled authentication modules.
688
684
689 :param username: username can be empty for headers auth
685 :param username: username can be empty for headers auth
690 :param password: password can be empty for headers auth
686 :param password: password can be empty for headers auth
691 :param environ: environ headers passed for headers auth
687 :param environ: environ headers passed for headers auth
692 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
688 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
693 :param skip_missing: ignores plugins that are in db but not in environment
689 :param skip_missing: ignores plugins that are in db but not in environment
694 :param registry: pyramid registry
690 :param registry: pyramid registry
695 :param acl_repo_name: name of repo for ACL checks
691 :param acl_repo_name: name of repo for ACL checks
696 :returns: None if auth failed, plugin_user dict if auth is correct
692 :returns: None if auth failed, plugin_user dict if auth is correct
697 """
693 """
698 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
694 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
699 raise ValueError(f'auth type must be on of http, vcs got "{auth_type}" instead')
695 raise ValueError(f'auth type must be on of http, vcs got "{auth_type}" instead')
700
696
701 auth_credentials = (username and password)
697 auth_credentials = (username and password)
702 headers_only = environ and not auth_credentials
698 headers_only = environ and not auth_credentials
703
699
704 authn_registry = get_authn_registry(registry)
700 authn_registry = get_authn_registry(registry)
705
701
706 plugins_to_check = authn_registry.get_plugins_for_authentication()
702 plugins_to_check = authn_registry.get_plugins_for_authentication()
707 log.debug('authentication: headers=%s, username_and_passwd=%s', headers_only, bool(auth_credentials))
703 log.debug('authentication: headers=%s, username_and_passwd=%s', headers_only, bool(auth_credentials))
708 log.debug('Starting ordered authentication chain using %s plugins',
704 log.debug('Starting ordered authentication chain using %s plugins',
709 [x.name for x in plugins_to_check])
705 [x.name for x in plugins_to_check])
710
706
711 for plugin in plugins_to_check:
707 for plugin in plugins_to_check:
712 plugin.set_auth_type(auth_type)
708 plugin.set_auth_type(auth_type)
713 plugin.set_calling_scope_repo(acl_repo_name)
709 plugin.set_calling_scope_repo(acl_repo_name)
714
710
715 if headers_only and not plugin.is_headers_auth:
711 if headers_only and not plugin.is_headers_auth:
716 log.debug('Auth type is for headers only and plugin `%s` is not '
712 log.debug('Auth type is for headers only and plugin `%s` is not '
717 'headers plugin, skipping...', plugin.get_id())
713 'headers plugin, skipping...', plugin.get_id())
718 continue
714 continue
719
715
720 log.debug('Trying authentication using ** %s **', plugin.get_id())
716 log.debug('Trying authentication using ** %s **', plugin.get_id())
721
717
722 # load plugin settings from RhodeCode database
718 # load plugin settings from RhodeCode database
723 plugin_settings = plugin.get_settings()
719 plugin_settings = plugin.get_settings()
724 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
720 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
725 log.debug('Plugin `%s` settings:%s', plugin.get_id(), plugin_sanitized_settings)
721 log.debug('Plugin `%s` settings:%s', plugin.get_id(), plugin_sanitized_settings)
726
722
727 # use plugin's method of user extraction.
723 # use plugin's method of user extraction.
728 user = plugin.get_user(username, environ=environ,
724 user = plugin.get_user(username, environ=environ,
729 settings=plugin_settings)
725 settings=plugin_settings)
730 display_user = user.username if user else username
726 display_user = user.username if user else username
731 log.debug(
727 log.debug(
732 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
728 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
733
729
734 if not plugin.allows_authentication_from(user):
730 if not plugin.allows_authentication_from(user):
735 log.debug('Plugin %s does not accept user `%s` for authentication',
731 log.debug('Plugin %s does not accept user `%s` for authentication',
736 plugin.get_id(), display_user)
732 plugin.get_id(), display_user)
737 continue
733 continue
738 else:
734 else:
739 log.debug('Plugin %s accepted user `%s` for authentication',
735 log.debug('Plugin %s accepted user `%s` for authentication',
740 plugin.get_id(), display_user)
736 plugin.get_id(), display_user)
741
737
742 log.info('Authenticating user `%s` using %s plugin',
738 log.info('Authenticating user `%s` using %s plugin',
743 display_user, plugin.get_id())
739 display_user, plugin.get_id())
744
740
745 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
741 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
746
742
747 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
743 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
748 plugin.get_id(), plugin_cache_active, cache_ttl)
744 plugin.get_id(), plugin_cache_active, cache_ttl)
749
745
750 user_id = user.user_id if user else 'no-user'
746 user_id = user.user_id if user else 'no-user'
751 # don't cache for empty users
747 # don't cache for empty users
752 plugin_cache_active = plugin_cache_active and user_id
748 plugin_cache_active = plugin_cache_active and user_id
753 cache_namespace_uid = f'cache_user_auth.{rc_cache.PERMISSIONS_CACHE_VER}.{user_id}'
749 cache_namespace_uid = f'cache_user_auth.{rc_cache.PERMISSIONS_CACHE_VER}.{user_id}'
754 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
750 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
755
751
756 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
752 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
757 expiration_time=cache_ttl,
753 expiration_time=cache_ttl,
758 condition=plugin_cache_active)
754 condition=plugin_cache_active)
759 def compute_auth(
755 def compute_auth(
760 cache_name, plugin_name, username, password):
756 cache_name, plugin_name, username, password):
761
757
762 # _authenticate is a wrapper for .auth() method of plugin.
758 # _authenticate is a wrapper for .auth() method of plugin.
763 # it checks if .auth() sends proper data.
759 # it checks if .auth() sends proper data.
764 # For RhodeCodeExternalAuthPlugin it also maps users to
760 # For RhodeCodeExternalAuthPlugin it also maps users to
765 # Database and maps the attributes returned from .auth()
761 # Database and maps the attributes returned from .auth()
766 # to RhodeCode database. If this function returns data
762 # to RhodeCode database. If this function returns data
767 # then auth is correct.
763 # then auth is correct.
768 log.debug('Running plugin `%s` _authenticate method '
764 log.debug('Running plugin `%s` _authenticate method '
769 'using username and password', plugin.get_id())
765 'using username and password', plugin.get_id())
770 return plugin._authenticate(
766 return plugin._authenticate(
771 user, username, password, plugin_settings,
767 user, username, password, plugin_settings,
772 environ=environ or {})
768 environ=environ or {})
773
769
774 start = time.time()
770 start = time.time()
775 # for environ based auth, password can be empty, but then the validation is
771 # for environ based auth, password can be empty, but then the validation is
776 # on the server that fills in the env data needed for authentication
772 # on the server that fills in the env data needed for authentication
777 plugin_user = compute_auth('auth', plugin.name, username, (password or ''))
773 plugin_user = compute_auth('auth', plugin.name, username, (password or ''))
778
774
779 auth_time = time.time() - start
775 auth_time = time.time() - start
780 log.debug('Authentication for plugin `%s` completed in %.4fs, '
776 log.debug('Authentication for plugin `%s` completed in %.4fs, '
781 'expiration time of fetched cache %.1fs.',
777 'expiration time of fetched cache %.1fs.',
782 plugin.get_id(), auth_time, cache_ttl,
778 plugin.get_id(), auth_time, cache_ttl,
783 extra={"plugin": plugin.get_id(), "time": auth_time})
779 extra={"plugin": plugin.get_id(), "time": auth_time})
784
780
785 log.debug('PLUGIN USER DATA: %s', plugin_user)
781 log.debug('PLUGIN USER DATA: %s', plugin_user)
786
782
787 statsd = StatsdClient.statsd
783 statsd = StatsdClient.statsd
788
784
789 if plugin_user:
785 if plugin_user:
790 log.debug('Plugin returned proper authentication data')
786 log.debug('Plugin returned proper authentication data')
791 if statsd:
787 if statsd:
792 elapsed_time_ms = round(1000.0 * auth_time) # use ms only
788 elapsed_time_ms = round(1000.0 * auth_time) # use ms only
793 statsd.incr('rhodecode_login_success_total')
789 statsd.incr('rhodecode_login_success_total')
794 statsd.timing("rhodecode_login_timing.histogram", elapsed_time_ms,
790 statsd.timing("rhodecode_login_timing.histogram", elapsed_time_ms,
795 tags=[f"plugin:{plugin.get_id()}"],
791 tags=[f"plugin:{plugin.get_id()}"],
796 use_decimals=False
792 use_decimals=False
797 )
793 )
798 return plugin_user
794 return plugin_user
799
795
800 # we failed to Auth because .auth() method didn't return proper user
796 # we failed to Auth because .auth() method didn't return proper user
801 log.debug("User `%s` failed to authenticate against %s",
797 log.debug("User `%s` failed to authenticate against %s",
802 display_user, plugin.get_id())
798 display_user, plugin.get_id())
803 if statsd:
799 if statsd:
804 statsd.incr('rhodecode_login_fail_total')
800 statsd.incr('rhodecode_login_fail_total')
805
801
806 # case when we failed to authenticate against all defined plugins
802 # case when we failed to authenticate against all defined plugins
807 return None
803 return None
808
804
809
805
810 def chop_at(s, sub, inclusive=False):
806 def chop_at(s, sub, inclusive=False):
811 """Truncate string ``s`` at the first occurrence of ``sub``.
807 """Truncate string ``s`` at the first occurrence of ``sub``.
812
808
813 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
809 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
814
810
815 >>> chop_at("plutocratic brats", "rat")
811 >>> chop_at("plutocratic brats", "rat")
816 'plutoc'
812 'plutoc'
817 >>> chop_at("plutocratic brats", "rat", True)
813 >>> chop_at("plutocratic brats", "rat", True)
818 'plutocrat'
814 'plutocrat'
819 """
815 """
820 pos = s.find(sub)
816 pos = s.find(sub)
821 if pos == -1:
817 if pos == -1:
822 return s
818 return s
823 if inclusive:
819 if inclusive:
824 return s[:pos+len(sub)]
820 return s[:pos+len(sub)]
825 return s[:pos]
821 return s[:pos]
@@ -1,220 +1,220 b''
1 # Copyright (C) 2012-2023 RhodeCode GmbH
1 # Copyright (C) 2012-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 """
19 """
20 RhodeCode authentication plugin for built in internal auth
20 RhodeCode authentication plugin for built in internal auth
21 """
21 """
22
22
23 import logging
23 import logging
24
24
25 import colander
25 import colander
26
26
27 from rhodecode.translation import _
27 from rhodecode.translation import _
28 from rhodecode.lib.utils2 import safe_bytes
28 from rhodecode.lib.utils2 import safe_bytes
29 from rhodecode.model.db import User
29 from rhodecode.model.db import User
30 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
30 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
31 from rhodecode.authentication.base import (
31 from rhodecode.authentication.base import (
32 RhodeCodeAuthPluginBase, hybrid_property, HTTP_TYPE, VCS_TYPE)
32 RhodeCodeAuthPluginBase, hybrid_property, HTTP_TYPE, VCS_TYPE)
33 from rhodecode.authentication.routes import AuthnPluginResourceBase
33 from rhodecode.authentication.routes import AuthnPluginResourceBase
34
34
35 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
36
36
37
37
38 def plugin_factory(plugin_id, *args, **kwargs):
38 def plugin_factory(plugin_id, *args, **kwargs):
39 plugin = RhodeCodeAuthPlugin(plugin_id)
39 plugin = RhodeCodeAuthPlugin(plugin_id)
40 return plugin
40 return plugin
41
41
42
42
43 class RhodecodeAuthnResource(AuthnPluginResourceBase):
43 class RhodecodeAuthnResource(AuthnPluginResourceBase):
44 pass
44 pass
45
45
46
46
47 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
47 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
48 uid = 'rhodecode'
48 uid = 'rhodecode'
49 AUTH_RESTRICTION_NONE = 'user_all'
49 AUTH_RESTRICTION_NONE = 'user_all'
50 AUTH_RESTRICTION_SUPER_ADMIN = 'user_super_admin'
50 AUTH_RESTRICTION_SUPER_ADMIN = 'user_super_admin'
51 AUTH_RESTRICTION_SCOPE_ALL = 'scope_all'
51 AUTH_RESTRICTION_SCOPE_ALL = 'scope_all'
52 AUTH_RESTRICTION_SCOPE_HTTP = 'scope_http'
52 AUTH_RESTRICTION_SCOPE_HTTP = 'scope_http'
53 AUTH_RESTRICTION_SCOPE_VCS = 'scope_vcs'
53 AUTH_RESTRICTION_SCOPE_VCS = 'scope_vcs'
54
54
55 def includeme(self, config):
55 def includeme(self, config):
56 config.add_authn_plugin(self)
56 config.add_authn_plugin(self)
57 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
57 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
58 config.add_view(
58 config.add_view(
59 'rhodecode.authentication.views.AuthnPluginViewBase',
59 'rhodecode.authentication.views.AuthnPluginViewBase',
60 attr='settings_get',
60 attr='settings_get',
61 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
61 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
62 request_method='GET',
62 request_method='GET',
63 route_name='auth_home',
63 route_name='auth_home',
64 context=RhodecodeAuthnResource)
64 context=RhodecodeAuthnResource)
65 config.add_view(
65 config.add_view(
66 'rhodecode.authentication.views.AuthnPluginViewBase',
66 'rhodecode.authentication.views.AuthnPluginViewBase',
67 attr='settings_post',
67 attr='settings_post',
68 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
68 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
69 request_method='POST',
69 request_method='POST',
70 route_name='auth_home',
70 route_name='auth_home',
71 context=RhodecodeAuthnResource)
71 context=RhodecodeAuthnResource)
72
72
73 def get_settings_schema(self):
73 def get_settings_schema(self):
74 return RhodeCodeSettingsSchema()
74 return RhodeCodeSettingsSchema()
75
75
76 def get_display_name(self, load_from_settings=False):
76 def get_display_name(self, load_from_settings=False):
77 return _('RhodeCode Internal')
77 return _('RhodeCode Internal')
78
78
79 @classmethod
79 @classmethod
80 def docs(cls):
80 def docs(cls):
81 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth.html"
81 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth.html"
82
82
83 @hybrid_property
83 @hybrid_property
84 def name(self):
84 def name(self):
85 return "rhodecode"
85 return "rhodecode"
86
86
87 def user_activation_state(self):
87 def user_activation_state(self):
88 def_user_perms = User.get_default_user().AuthUser().permissions['global']
88 def_user_perms = User.get_default_user().AuthUser().permissions['global']
89 return 'hg.register.auto_activate' in def_user_perms
89 return 'hg.register.auto_activate' in def_user_perms
90
90
91 def allows_authentication_from(
91 def allows_authentication_from(
92 self, user, allows_non_existing_user=True,
92 self, user, allows_non_existing_user=True,
93 allowed_auth_plugins=None, allowed_auth_sources=None):
93 allowed_auth_plugins=None, allowed_auth_sources=None):
94 """
94 """
95 Custom method for this auth that doesn't accept non existing users.
95 Custom method for this auth that doesn't accept non existing users.
96 We know that user exists in our database.
96 We know that user exists in our database.
97 """
97 """
98 allows_non_existing_user = False
98 allows_non_existing_user = False
99 return super().allows_authentication_from(
99 return super().allows_authentication_from(
100 user, allows_non_existing_user=allows_non_existing_user)
100 user, allows_non_existing_user=allows_non_existing_user)
101
101
102 def auth(self, userobj, username, password, settings, **kwargs):
102 def auth(self, userobj, username, password, settings, **kwargs):
103 if not userobj:
103 if not userobj:
104 log.debug('userobj was:%s skipping', userobj)
104 log.debug('userobj was:%s skipping', userobj)
105 return None
105 return None
106
106
107 if userobj.extern_type != self.name:
107 if userobj.extern_type != self.name:
108 log.warning("userobj:%s extern_type mismatch got:`%s` expected:`%s`",
108 log.warning("userobj:%s extern_type mismatch got:`%s` expected:`%s`",
109 userobj, userobj.extern_type, self.name)
109 userobj, userobj.extern_type, self.name)
110 return None
110 return None
111
111
112 # check scope of auth
112 # check scope of auth
113 scope_restriction = settings.get('scope_restriction', '')
113 scope_restriction = settings.get('scope_restriction', '')
114
114
115 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_HTTP \
115 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_HTTP \
116 and self.auth_type != HTTP_TYPE:
116 and self.auth_type != HTTP_TYPE:
117 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
117 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
118 userobj, self.auth_type, scope_restriction)
118 userobj, self.auth_type, scope_restriction)
119 return None
119 return None
120
120
121 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_VCS \
121 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_VCS \
122 and self.auth_type != VCS_TYPE:
122 and self.auth_type != VCS_TYPE:
123 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
123 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
124 userobj, self.auth_type, scope_restriction)
124 userobj, self.auth_type, scope_restriction)
125 return None
125 return None
126
126
127 # check super-admin restriction
127 # check super-admin restriction
128 auth_restriction = settings.get('auth_restriction', '')
128 auth_restriction = settings.get('auth_restriction', '')
129
129
130 if auth_restriction == self.AUTH_RESTRICTION_SUPER_ADMIN \
130 if auth_restriction == self.AUTH_RESTRICTION_SUPER_ADMIN \
131 and userobj.admin is False:
131 and userobj.admin is False:
132 log.warning("userobj:%s is not super-admin and auth restriction is set to %s",
132 log.warning("userobj:%s is not super-admin and auth restriction is set to %s",
133 userobj, auth_restriction)
133 userobj, auth_restriction)
134 return None
134 return None
135
135
136 user_attrs = {
136 user_attrs = {
137 "username": userobj.username,
137 "username": userobj.username,
138 "firstname": userobj.firstname,
138 "firstname": userobj.firstname,
139 "lastname": userobj.lastname,
139 "lastname": userobj.lastname,
140 "groups": [],
140 "groups": [],
141 'user_group_sync': False,
141 'user_group_sync': False,
142 "email": userobj.email,
142 "email": userobj.email,
143 "admin": userobj.admin,
143 "admin": userobj.admin,
144 "active": userobj.active,
144 "active": userobj.active,
145 "active_from_extern": userobj.active,
145 "active_from_extern": userobj.active,
146 "extern_name": userobj.user_id,
146 "extern_name": userobj.user_id,
147 "extern_type": userobj.extern_type,
147 "extern_type": userobj.extern_type,
148 }
148 }
149
149
150 log.debug("User attributes:%s", user_attrs)
150 log.debug("User attributes:%s", user_attrs)
151 if userobj.active:
151 if userobj.active:
152 from rhodecode.lib import auth
152 from rhodecode.lib import auth
153 crypto_backend = auth.crypto_backend()
153 crypto_backend = auth.crypto_backend()
154 password_encoded = safe_bytes(password)
154 password_encoded = safe_bytes(password)
155 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
155 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
156 password_encoded, userobj.password or '')
156 password_encoded, userobj.password or '')
157
157
158 if password_match and new_hash:
158 if password_match and new_hash:
159 log.debug('user %s properly authenticated, but '
159 log.debug('user %s properly authenticated, but '
160 'requires hash change to bcrypt', userobj)
160 'requires hash change to bcrypt', userobj)
161 # if password match, and we use OLD deprecated hash,
161 # if password match, and we use OLD deprecated hash,
162 # we should migrate this user hash password to the new hash
162 # we should migrate this user hash password to the new hash
163 # we store the new returned by hash_check_with_upgrade function
163 # we store the new returned by hash_check_with_upgrade function
164 user_attrs['_hash_migrate'] = new_hash
164 user_attrs['_hash_migrate'] = new_hash
165
165
166 if userobj.username == User.DEFAULT_USER and userobj.active:
166 if userobj.username == User.DEFAULT_USER and userobj.active:
167 log.info('user `%s` authenticated correctly as anonymous user',
167 log.info('user `%s` authenticated correctly as anonymous user',
168 userobj.username,
168 userobj.username,
169 extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode_anon", "username": userobj.username})
169 extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode_anon", "username": userobj.username})
170 return user_attrs
170 return user_attrs
171
171
172 elif userobj.username == username and password_match:
172 elif (userobj.username == username or userobj.email == username) and password_match:
173 log.info('user `%s` authenticated correctly', userobj.username,
173 log.info('user `%s` authenticated correctly', userobj.username,
174 extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode", "username": userobj.username})
174 extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode", "username": userobj.username})
175 return user_attrs
175 return user_attrs
176 log.warning("user `%s` used a wrong password when "
176 log.warning("user `%s` used a wrong password when "
177 "authenticating on this plugin", userobj.username)
177 "authenticating on this plugin", userobj.username)
178 return None
178 return None
179 else:
179 else:
180 log.warning('user `%s` failed to authenticate via %s, reason: account not '
180 log.warning('user `%s` failed to authenticate via %s, reason: account not '
181 'active.', username, self.name)
181 'active.', username, self.name)
182 return None
182 return None
183
183
184
184
185 class RhodeCodeSettingsSchema(AuthnPluginSettingsSchemaBase):
185 class RhodeCodeSettingsSchema(AuthnPluginSettingsSchemaBase):
186
186
187 auth_restriction_choices = [
187 auth_restriction_choices = [
188 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_NONE, 'All users'),
188 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_NONE, 'All users'),
189 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN, 'Super admins only'),
189 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN, 'Super admins only'),
190 ]
190 ]
191
191
192 auth_scope_choices = [
192 auth_scope_choices = [
193 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_ALL, 'HTTP and VCS'),
193 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_ALL, 'HTTP and VCS'),
194 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_HTTP, 'HTTP only'),
194 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_HTTP, 'HTTP only'),
195 ]
195 ]
196
196
197 auth_restriction = colander.SchemaNode(
197 auth_restriction = colander.SchemaNode(
198 colander.String(),
198 colander.String(),
199 default=auth_restriction_choices[0],
199 default=auth_restriction_choices[0],
200 description=_('Allowed user types for authentication using this plugin.'),
200 description=_('Allowed user types for authentication using this plugin.'),
201 title=_('User restriction'),
201 title=_('User restriction'),
202 validator=colander.OneOf([x[0] for x in auth_restriction_choices]),
202 validator=colander.OneOf([x[0] for x in auth_restriction_choices]),
203 widget='select_with_labels',
203 widget='select_with_labels',
204 choices=auth_restriction_choices
204 choices=auth_restriction_choices
205 )
205 )
206 scope_restriction = colander.SchemaNode(
206 scope_restriction = colander.SchemaNode(
207 colander.String(),
207 colander.String(),
208 default=auth_scope_choices[0],
208 default=auth_scope_choices[0],
209 description=_('Allowed protocols for authentication using this plugin. '
209 description=_('Allowed protocols for authentication using this plugin. '
210 'VCS means GIT/HG/SVN. HTTP is web based login.'),
210 'VCS means GIT/HG/SVN. HTTP is web based login.'),
211 title=_('Scope restriction'),
211 title=_('Scope restriction'),
212 validator=colander.OneOf([x[0] for x in auth_scope_choices]),
212 validator=colander.OneOf([x[0] for x in auth_scope_choices]),
213 widget='select_with_labels',
213 widget='select_with_labels',
214 choices=auth_scope_choices
214 choices=auth_scope_choices
215 )
215 )
216
216
217
217
218 def includeme(config):
218 def includeme(config):
219 plugin_id = f'egg:rhodecode-enterprise-ce#{RhodeCodeAuthPlugin.uid}'
219 plugin_id = f'egg:rhodecode-enterprise-ce#{RhodeCodeAuthPlugin.uid}'
220 plugin_factory(plugin_id).includeme(config)
220 plugin_factory(plugin_id).includeme(config)
@@ -1,5884 +1,5890 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 """
19 """
20 Database Models for RhodeCode Enterprise
20 Database Models for RhodeCode Enterprise
21 """
21 """
22
22
23 import re
23 import re
24 import os
24 import os
25 import time
25 import time
26 import string
26 import string
27 import logging
27 import logging
28 import datetime
28 import datetime
29 import uuid
29 import uuid
30 import warnings
30 import warnings
31 import ipaddress
31 import ipaddress
32 import functools
32 import functools
33 import traceback
33 import traceback
34 import collections
34 import collections
35
35
36 from sqlalchemy import (
36 from sqlalchemy import (
37 or_, and_, not_, func, cast, TypeDecorator, event, select,
37 or_, and_, not_, func, cast, TypeDecorator, event, select,
38 true, false, null,
38 true, false, null, union_all,
39 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
39 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
40 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
40 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
41 Text, Float, PickleType, BigInteger)
41 Text, Float, PickleType, BigInteger)
42 from sqlalchemy.sql.expression import case
42 from sqlalchemy.sql.expression import case
43 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
43 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
44 from sqlalchemy.orm import (
44 from sqlalchemy.orm import (
45 relationship, lazyload, joinedload, class_mapper, validates, aliased, load_only)
45 relationship, lazyload, joinedload, class_mapper, validates, aliased, load_only)
46 from sqlalchemy.ext.declarative import declared_attr
46 from sqlalchemy.ext.declarative import declared_attr
47 from sqlalchemy.ext.hybrid import hybrid_property
47 from sqlalchemy.ext.hybrid import hybrid_property
48 from sqlalchemy.exc import IntegrityError # pragma: no cover
48 from sqlalchemy.exc import IntegrityError # pragma: no cover
49 from sqlalchemy.dialects.mysql import LONGTEXT
49 from sqlalchemy.dialects.mysql import LONGTEXT
50 from zope.cachedescriptors.property import Lazy as LazyProperty
50 from zope.cachedescriptors.property import Lazy as LazyProperty
51 from pyramid.threadlocal import get_current_request
51 from pyramid.threadlocal import get_current_request
52 from webhelpers2.text import remove_formatting
52 from webhelpers2.text import remove_formatting
53
53
54 from rhodecode.lib.str_utils import safe_bytes
54 from rhodecode.lib.str_utils import safe_bytes
55 from rhodecode.translation import _
55 from rhodecode.translation import _
56 from rhodecode.lib.vcs import get_vcs_instance, VCSError
56 from rhodecode.lib.vcs import get_vcs_instance, VCSError
57 from rhodecode.lib.vcs.backends.base import (
57 from rhodecode.lib.vcs.backends.base import (
58 EmptyCommit, Reference, unicode_to_reference, reference_to_unicode)
58 EmptyCommit, Reference, unicode_to_reference, reference_to_unicode)
59 from rhodecode.lib.utils2 import (
59 from rhodecode.lib.utils2 import (
60 str2bool, safe_str, get_commit_safe, sha1_safe,
60 str2bool, safe_str, get_commit_safe, sha1_safe,
61 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
61 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
62 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time)
62 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time)
63 from rhodecode.lib.jsonalchemy import (
63 from rhodecode.lib.jsonalchemy import (
64 MutationObj, MutationList, JsonType, JsonRaw)
64 MutationObj, MutationList, JsonType, JsonRaw)
65 from rhodecode.lib.hash_utils import sha1
65 from rhodecode.lib.hash_utils import sha1
66 from rhodecode.lib import ext_json
66 from rhodecode.lib import ext_json
67 from rhodecode.lib import enc_utils
67 from rhodecode.lib import enc_utils
68 from rhodecode.lib.ext_json import json, str_json
68 from rhodecode.lib.ext_json import json, str_json
69 from rhodecode.lib.caching_query import FromCache
69 from rhodecode.lib.caching_query import FromCache
70 from rhodecode.lib.exceptions import (
70 from rhodecode.lib.exceptions import (
71 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
71 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
72 from rhodecode.model.meta import Base, Session
72 from rhodecode.model.meta import Base, Session
73
73
74 URL_SEP = '/'
74 URL_SEP = '/'
75 log = logging.getLogger(__name__)
75 log = logging.getLogger(__name__)
76
76
77 # =============================================================================
77 # =============================================================================
78 # BASE CLASSES
78 # BASE CLASSES
79 # =============================================================================
79 # =============================================================================
80
80
81 # this is propagated from .ini file rhodecode.encrypted_values.secret or
81 # this is propagated from .ini file rhodecode.encrypted_values.secret or
82 # beaker.session.secret if first is not set.
82 # beaker.session.secret if first is not set.
83 # and initialized at environment.py
83 # and initialized at environment.py
84 ENCRYPTION_KEY: bytes = b''
84 ENCRYPTION_KEY: bytes = b''
85
85
86 # used to sort permissions by types, '#' used here is not allowed to be in
86 # used to sort permissions by types, '#' used here is not allowed to be in
87 # usernames, and it's very early in sorted string.printable table.
87 # usernames, and it's very early in sorted string.printable table.
88 PERMISSION_TYPE_SORT = {
88 PERMISSION_TYPE_SORT = {
89 'admin': '####',
89 'admin': '####',
90 'write': '###',
90 'write': '###',
91 'read': '##',
91 'read': '##',
92 'none': '#',
92 'none': '#',
93 }
93 }
94
94
95
95
96 def display_user_sort(obj):
96 def display_user_sort(obj):
97 """
97 """
98 Sort function used to sort permissions in .permissions() function of
98 Sort function used to sort permissions in .permissions() function of
99 Repository, RepoGroup, UserGroup. Also it put the default user in front
99 Repository, RepoGroup, UserGroup. Also it put the default user in front
100 of all other resources
100 of all other resources
101 """
101 """
102
102
103 if obj.username == User.DEFAULT_USER:
103 if obj.username == User.DEFAULT_USER:
104 return '#####'
104 return '#####'
105 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
105 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
106 extra_sort_num = '1' # default
106 extra_sort_num = '1' # default
107
107
108 # NOTE(dan): inactive duplicates goes last
108 # NOTE(dan): inactive duplicates goes last
109 if getattr(obj, 'duplicate_perm', None):
109 if getattr(obj, 'duplicate_perm', None):
110 extra_sort_num = '9'
110 extra_sort_num = '9'
111 return prefix + extra_sort_num + obj.username
111 return prefix + extra_sort_num + obj.username
112
112
113
113
114 def display_user_group_sort(obj):
114 def display_user_group_sort(obj):
115 """
115 """
116 Sort function used to sort permissions in .permissions() function of
116 Sort function used to sort permissions in .permissions() function of
117 Repository, RepoGroup, UserGroup. Also it put the default user in front
117 Repository, RepoGroup, UserGroup. Also it put the default user in front
118 of all other resources
118 of all other resources
119 """
119 """
120
120
121 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
121 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
122 return prefix + obj.users_group_name
122 return prefix + obj.users_group_name
123
123
124
124
125 def _hash_key(k):
125 def _hash_key(k):
126 return sha1_safe(k)
126 return sha1_safe(k)
127
127
128
128
129 def in_filter_generator(qry, items, limit=500):
129 def in_filter_generator(qry, items, limit=500):
130 """
130 """
131 Splits IN() into multiple with OR
131 Splits IN() into multiple with OR
132 e.g.::
132 e.g.::
133 cnt = Repository.query().filter(
133 cnt = Repository.query().filter(
134 or_(
134 or_(
135 *in_filter_generator(Repository.repo_id, range(100000))
135 *in_filter_generator(Repository.repo_id, range(100000))
136 )).count()
136 )).count()
137 """
137 """
138 if not items:
138 if not items:
139 # empty list will cause empty query which might cause security issues
139 # empty list will cause empty query which might cause security issues
140 # this can lead to hidden unpleasant results
140 # this can lead to hidden unpleasant results
141 items = [-1]
141 items = [-1]
142
142
143 parts = []
143 parts = []
144 for chunk in range(0, len(items), limit):
144 for chunk in range(0, len(items), limit):
145 parts.append(
145 parts.append(
146 qry.in_(items[chunk: chunk + limit])
146 qry.in_(items[chunk: chunk + limit])
147 )
147 )
148
148
149 return parts
149 return parts
150
150
151
151
152 base_table_args = {
152 base_table_args = {
153 'extend_existing': True,
153 'extend_existing': True,
154 'mysql_engine': 'InnoDB',
154 'mysql_engine': 'InnoDB',
155 'mysql_charset': 'utf8',
155 'mysql_charset': 'utf8',
156 'sqlite_autoincrement': True
156 'sqlite_autoincrement': True
157 }
157 }
158
158
159
159
160 class EncryptedTextValue(TypeDecorator):
160 class EncryptedTextValue(TypeDecorator):
161 """
161 """
162 Special column for encrypted long text data, use like::
162 Special column for encrypted long text data, use like::
163
163
164 value = Column("encrypted_value", EncryptedValue(), nullable=False)
164 value = Column("encrypted_value", EncryptedValue(), nullable=False)
165
165
166 This column is intelligent so if value is in unencrypted form it return
166 This column is intelligent so if value is in unencrypted form it return
167 unencrypted form, but on save it always encrypts
167 unencrypted form, but on save it always encrypts
168 """
168 """
169 cache_ok = True
169 cache_ok = True
170 impl = Text
170 impl = Text
171
171
172 def process_bind_param(self, value, dialect):
172 def process_bind_param(self, value, dialect):
173 """
173 """
174 Setter for storing value
174 Setter for storing value
175 """
175 """
176 import rhodecode
176 import rhodecode
177 if not value:
177 if not value:
178 return value
178 return value
179
179
180 # protect against double encrypting if values is already encrypted
180 # protect against double encrypting if values is already encrypted
181 if value.startswith('enc$aes$') \
181 if value.startswith('enc$aes$') \
182 or value.startswith('enc$aes_hmac$') \
182 or value.startswith('enc$aes_hmac$') \
183 or value.startswith('enc2$'):
183 or value.startswith('enc2$'):
184 raise ValueError('value needs to be in unencrypted format, '
184 raise ValueError('value needs to be in unencrypted format, '
185 'ie. not starting with enc$ or enc2$')
185 'ie. not starting with enc$ or enc2$')
186
186
187 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
187 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
188 bytes_val = enc_utils.encrypt_value(value, enc_key=ENCRYPTION_KEY, algo=algo)
188 bytes_val = enc_utils.encrypt_value(value, enc_key=ENCRYPTION_KEY, algo=algo)
189 return safe_str(bytes_val)
189 return safe_str(bytes_val)
190
190
191 def process_result_value(self, value, dialect):
191 def process_result_value(self, value, dialect):
192 """
192 """
193 Getter for retrieving value
193 Getter for retrieving value
194 """
194 """
195
195
196 import rhodecode
196 import rhodecode
197 if not value:
197 if not value:
198 return value
198 return value
199
199
200 enc_strict_mode = rhodecode.ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True)
200 enc_strict_mode = rhodecode.ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True)
201
201
202 bytes_val = enc_utils.decrypt_value(value, enc_key=ENCRYPTION_KEY, strict_mode=enc_strict_mode)
202 bytes_val = enc_utils.decrypt_value(value, enc_key=ENCRYPTION_KEY, strict_mode=enc_strict_mode)
203
203
204 return safe_str(bytes_val)
204 return safe_str(bytes_val)
205
205
206
206
207 class BaseModel(object):
207 class BaseModel(object):
208 """
208 """
209 Base Model for all classes
209 Base Model for all classes
210 """
210 """
211
211
212 @classmethod
212 @classmethod
213 def _get_keys(cls):
213 def _get_keys(cls):
214 """return column names for this model """
214 """return column names for this model """
215 return class_mapper(cls).c.keys()
215 return class_mapper(cls).c.keys()
216
216
217 def get_dict(self):
217 def get_dict(self):
218 """
218 """
219 return dict with keys and values corresponding
219 return dict with keys and values corresponding
220 to this model data """
220 to this model data """
221
221
222 d = {}
222 d = {}
223 for k in self._get_keys():
223 for k in self._get_keys():
224 d[k] = getattr(self, k)
224 d[k] = getattr(self, k)
225
225
226 # also use __json__() if present to get additional fields
226 # also use __json__() if present to get additional fields
227 _json_attr = getattr(self, '__json__', None)
227 _json_attr = getattr(self, '__json__', None)
228 if _json_attr:
228 if _json_attr:
229 # update with attributes from __json__
229 # update with attributes from __json__
230 if callable(_json_attr):
230 if callable(_json_attr):
231 _json_attr = _json_attr()
231 _json_attr = _json_attr()
232 for k, val in _json_attr.items():
232 for k, val in _json_attr.items():
233 d[k] = val
233 d[k] = val
234 return d
234 return d
235
235
236 def get_appstruct(self):
236 def get_appstruct(self):
237 """return list with keys and values tuples corresponding
237 """return list with keys and values tuples corresponding
238 to this model data """
238 to this model data """
239
239
240 lst = []
240 lst = []
241 for k in self._get_keys():
241 for k in self._get_keys():
242 lst.append((k, getattr(self, k),))
242 lst.append((k, getattr(self, k),))
243 return lst
243 return lst
244
244
245 def populate_obj(self, populate_dict):
245 def populate_obj(self, populate_dict):
246 """populate model with data from given populate_dict"""
246 """populate model with data from given populate_dict"""
247
247
248 for k in self._get_keys():
248 for k in self._get_keys():
249 if k in populate_dict:
249 if k in populate_dict:
250 setattr(self, k, populate_dict[k])
250 setattr(self, k, populate_dict[k])
251
251
252 @classmethod
252 @classmethod
253 def query(cls):
253 def query(cls):
254 return Session().query(cls)
254 return Session().query(cls)
255
255
256 @classmethod
256 @classmethod
257 def select(cls, custom_cls=None):
257 def select(cls, custom_cls=None):
258 """
258 """
259 stmt = cls.select().where(cls.user_id==1)
259 stmt = cls.select().where(cls.user_id==1)
260 # optionally
260 # optionally
261 stmt = cls.select(User.user_id).where(cls.user_id==1)
261 stmt = cls.select(User.user_id).where(cls.user_id==1)
262 result = cls.execute(stmt) | cls.scalars(stmt)
262 result = cls.execute(stmt) | cls.scalars(stmt)
263 """
263 """
264
264
265 if custom_cls:
265 if custom_cls:
266 stmt = select(custom_cls)
266 stmt = select(custom_cls)
267 else:
267 else:
268 stmt = select(cls)
268 stmt = select(cls)
269 return stmt
269 return stmt
270
270
271 @classmethod
271 @classmethod
272 def execute(cls, stmt):
272 def execute(cls, stmt):
273 return Session().execute(stmt)
273 return Session().execute(stmt)
274
274
275 @classmethod
275 @classmethod
276 def scalars(cls, stmt):
276 def scalars(cls, stmt):
277 return Session().scalars(stmt)
277 return Session().scalars(stmt)
278
278
279 @classmethod
279 @classmethod
280 def get(cls, id_):
280 def get(cls, id_):
281 if id_:
281 if id_:
282 return cls.query().get(id_)
282 return cls.query().get(id_)
283
283
284 @classmethod
284 @classmethod
285 def get_or_404(cls, id_):
285 def get_or_404(cls, id_):
286 from pyramid.httpexceptions import HTTPNotFound
286 from pyramid.httpexceptions import HTTPNotFound
287
287
288 try:
288 try:
289 id_ = int(id_)
289 id_ = int(id_)
290 except (TypeError, ValueError):
290 except (TypeError, ValueError):
291 raise HTTPNotFound()
291 raise HTTPNotFound()
292
292
293 res = cls.query().get(id_)
293 res = cls.query().get(id_)
294 if not res:
294 if not res:
295 raise HTTPNotFound()
295 raise HTTPNotFound()
296 return res
296 return res
297
297
298 @classmethod
298 @classmethod
299 def getAll(cls):
299 def getAll(cls):
300 # deprecated and left for backward compatibility
300 # deprecated and left for backward compatibility
301 return cls.get_all()
301 return cls.get_all()
302
302
303 @classmethod
303 @classmethod
304 def get_all(cls):
304 def get_all(cls):
305 return cls.query().all()
305 return cls.query().all()
306
306
307 @classmethod
307 @classmethod
308 def delete(cls, id_):
308 def delete(cls, id_):
309 obj = cls.query().get(id_)
309 obj = cls.query().get(id_)
310 Session().delete(obj)
310 Session().delete(obj)
311
311
312 @classmethod
312 @classmethod
313 def identity_cache(cls, session, attr_name, value):
313 def identity_cache(cls, session, attr_name, value):
314 exist_in_session = []
314 exist_in_session = []
315 for (item_cls, pkey), instance in session.identity_map.items():
315 for (item_cls, pkey), instance in session.identity_map.items():
316 if cls == item_cls and getattr(instance, attr_name) == value:
316 if cls == item_cls and getattr(instance, attr_name) == value:
317 exist_in_session.append(instance)
317 exist_in_session.append(instance)
318 if exist_in_session:
318 if exist_in_session:
319 if len(exist_in_session) == 1:
319 if len(exist_in_session) == 1:
320 return exist_in_session[0]
320 return exist_in_session[0]
321 log.exception(
321 log.exception(
322 'multiple objects with attr %s and '
322 'multiple objects with attr %s and '
323 'value %s found with same name: %r',
323 'value %s found with same name: %r',
324 attr_name, value, exist_in_session)
324 attr_name, value, exist_in_session)
325
325
326 @property
326 @property
327 def cls_name(self):
327 def cls_name(self):
328 return self.__class__.__name__
328 return self.__class__.__name__
329
329
330 def __repr__(self):
330 def __repr__(self):
331 return f'<DB:{self.cls_name}>'
331 return f'<DB:{self.cls_name}>'
332
332
333
333
334 class RhodeCodeSetting(Base, BaseModel):
334 class RhodeCodeSetting(Base, BaseModel):
335 __tablename__ = 'rhodecode_settings'
335 __tablename__ = 'rhodecode_settings'
336 __table_args__ = (
336 __table_args__ = (
337 UniqueConstraint('app_settings_name'),
337 UniqueConstraint('app_settings_name'),
338 base_table_args
338 base_table_args
339 )
339 )
340
340
341 SETTINGS_TYPES = {
341 SETTINGS_TYPES = {
342 'str': safe_str,
342 'str': safe_str,
343 'int': safe_int,
343 'int': safe_int,
344 'unicode': safe_str,
344 'unicode': safe_str,
345 'bool': str2bool,
345 'bool': str2bool,
346 'list': functools.partial(aslist, sep=',')
346 'list': functools.partial(aslist, sep=',')
347 }
347 }
348 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
348 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
349 GLOBAL_CONF_KEY = 'app_settings'
349 GLOBAL_CONF_KEY = 'app_settings'
350
350
351 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
351 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
352 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
352 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
353 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
353 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
354 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
354 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
355
355
356 def __init__(self, key='', val='', type='unicode'):
356 def __init__(self, key='', val='', type='unicode'):
357 self.app_settings_name = key
357 self.app_settings_name = key
358 self.app_settings_type = type
358 self.app_settings_type = type
359 self.app_settings_value = val
359 self.app_settings_value = val
360
360
361 @validates('_app_settings_value')
361 @validates('_app_settings_value')
362 def validate_settings_value(self, key, val):
362 def validate_settings_value(self, key, val):
363 assert type(val) == str
363 assert type(val) == str
364 return val
364 return val
365
365
366 @hybrid_property
366 @hybrid_property
367 def app_settings_value(self):
367 def app_settings_value(self):
368 v = self._app_settings_value
368 v = self._app_settings_value
369 _type = self.app_settings_type
369 _type = self.app_settings_type
370 if _type:
370 if _type:
371 _type = self.app_settings_type.split('.')[0]
371 _type = self.app_settings_type.split('.')[0]
372 # decode the encrypted value
372 # decode the encrypted value
373 if 'encrypted' in self.app_settings_type:
373 if 'encrypted' in self.app_settings_type:
374 cipher = EncryptedTextValue()
374 cipher = EncryptedTextValue()
375 v = safe_str(cipher.process_result_value(v, None))
375 v = safe_str(cipher.process_result_value(v, None))
376
376
377 converter = self.SETTINGS_TYPES.get(_type) or \
377 converter = self.SETTINGS_TYPES.get(_type) or \
378 self.SETTINGS_TYPES['unicode']
378 self.SETTINGS_TYPES['unicode']
379 return converter(v)
379 return converter(v)
380
380
381 @app_settings_value.setter
381 @app_settings_value.setter
382 def app_settings_value(self, val):
382 def app_settings_value(self, val):
383 """
383 """
384 Setter that will always make sure we use unicode in app_settings_value
384 Setter that will always make sure we use unicode in app_settings_value
385
385
386 :param val:
386 :param val:
387 """
387 """
388 val = safe_str(val)
388 val = safe_str(val)
389 # encode the encrypted value
389 # encode the encrypted value
390 if 'encrypted' in self.app_settings_type:
390 if 'encrypted' in self.app_settings_type:
391 cipher = EncryptedTextValue()
391 cipher = EncryptedTextValue()
392 val = safe_str(cipher.process_bind_param(val, None))
392 val = safe_str(cipher.process_bind_param(val, None))
393 self._app_settings_value = val
393 self._app_settings_value = val
394
394
395 @hybrid_property
395 @hybrid_property
396 def app_settings_type(self):
396 def app_settings_type(self):
397 return self._app_settings_type
397 return self._app_settings_type
398
398
399 @app_settings_type.setter
399 @app_settings_type.setter
400 def app_settings_type(self, val):
400 def app_settings_type(self, val):
401 if val.split('.')[0] not in self.SETTINGS_TYPES:
401 if val.split('.')[0] not in self.SETTINGS_TYPES:
402 raise Exception('type must be one of %s got %s'
402 raise Exception('type must be one of %s got %s'
403 % (self.SETTINGS_TYPES.keys(), val))
403 % (self.SETTINGS_TYPES.keys(), val))
404 self._app_settings_type = val
404 self._app_settings_type = val
405
405
406 @classmethod
406 @classmethod
407 def get_by_prefix(cls, prefix):
407 def get_by_prefix(cls, prefix):
408 return RhodeCodeSetting.query()\
408 return RhodeCodeSetting.query()\
409 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
409 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
410 .all()
410 .all()
411
411
412 def __repr__(self):
412 def __repr__(self):
413 return "<%s('%s:%s[%s]')>" % (
413 return "<%s('%s:%s[%s]')>" % (
414 self.cls_name,
414 self.cls_name,
415 self.app_settings_name, self.app_settings_value,
415 self.app_settings_name, self.app_settings_value,
416 self.app_settings_type
416 self.app_settings_type
417 )
417 )
418
418
419
419
420 class RhodeCodeUi(Base, BaseModel):
420 class RhodeCodeUi(Base, BaseModel):
421 __tablename__ = 'rhodecode_ui'
421 __tablename__ = 'rhodecode_ui'
422 __table_args__ = (
422 __table_args__ = (
423 UniqueConstraint('ui_key'),
423 UniqueConstraint('ui_key'),
424 base_table_args
424 base_table_args
425 )
425 )
426 # Sync those values with vcsserver.config.hooks
426 # Sync those values with vcsserver.config.hooks
427
427
428 HOOK_REPO_SIZE = 'changegroup.repo_size'
428 HOOK_REPO_SIZE = 'changegroup.repo_size'
429 # HG
429 # HG
430 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
430 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
431 HOOK_PULL = 'outgoing.pull_logger'
431 HOOK_PULL = 'outgoing.pull_logger'
432 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
432 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
433 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
433 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
434 HOOK_PUSH = 'changegroup.push_logger'
434 HOOK_PUSH = 'changegroup.push_logger'
435 HOOK_PUSH_KEY = 'pushkey.key_push'
435 HOOK_PUSH_KEY = 'pushkey.key_push'
436
436
437 HOOKS_BUILTIN = [
437 HOOKS_BUILTIN = [
438 HOOK_PRE_PULL,
438 HOOK_PRE_PULL,
439 HOOK_PULL,
439 HOOK_PULL,
440 HOOK_PRE_PUSH,
440 HOOK_PRE_PUSH,
441 HOOK_PRETX_PUSH,
441 HOOK_PRETX_PUSH,
442 HOOK_PUSH,
442 HOOK_PUSH,
443 HOOK_PUSH_KEY,
443 HOOK_PUSH_KEY,
444 ]
444 ]
445
445
446 # TODO: johbo: Unify way how hooks are configured for git and hg,
446 # TODO: johbo: Unify way how hooks are configured for git and hg,
447 # git part is currently hardcoded.
447 # git part is currently hardcoded.
448
448
449 # SVN PATTERNS
449 # SVN PATTERNS
450 SVN_BRANCH_ID = 'vcs_svn_branch'
450 SVN_BRANCH_ID = 'vcs_svn_branch'
451 SVN_TAG_ID = 'vcs_svn_tag'
451 SVN_TAG_ID = 'vcs_svn_tag'
452
452
453 ui_id = Column(
453 ui_id = Column(
454 "ui_id", Integer(), nullable=False, unique=True, default=None,
454 "ui_id", Integer(), nullable=False, unique=True, default=None,
455 primary_key=True)
455 primary_key=True)
456 ui_section = Column(
456 ui_section = Column(
457 "ui_section", String(255), nullable=True, unique=None, default=None)
457 "ui_section", String(255), nullable=True, unique=None, default=None)
458 ui_key = Column(
458 ui_key = Column(
459 "ui_key", String(255), nullable=True, unique=None, default=None)
459 "ui_key", String(255), nullable=True, unique=None, default=None)
460 ui_value = Column(
460 ui_value = Column(
461 "ui_value", String(255), nullable=True, unique=None, default=None)
461 "ui_value", String(255), nullable=True, unique=None, default=None)
462 ui_active = Column(
462 ui_active = Column(
463 "ui_active", Boolean(), nullable=True, unique=None, default=True)
463 "ui_active", Boolean(), nullable=True, unique=None, default=True)
464
464
465 def __repr__(self):
465 def __repr__(self):
466 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.ui_section,
466 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.ui_section,
467 self.ui_key, self.ui_value)
467 self.ui_key, self.ui_value)
468
468
469
469
470 class RepoRhodeCodeSetting(Base, BaseModel):
470 class RepoRhodeCodeSetting(Base, BaseModel):
471 __tablename__ = 'repo_rhodecode_settings'
471 __tablename__ = 'repo_rhodecode_settings'
472 __table_args__ = (
472 __table_args__ = (
473 UniqueConstraint(
473 UniqueConstraint(
474 'app_settings_name', 'repository_id',
474 'app_settings_name', 'repository_id',
475 name='uq_repo_rhodecode_setting_name_repo_id'),
475 name='uq_repo_rhodecode_setting_name_repo_id'),
476 base_table_args
476 base_table_args
477 )
477 )
478
478
479 repository_id = Column(
479 repository_id = Column(
480 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
480 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
481 nullable=False)
481 nullable=False)
482 app_settings_id = Column(
482 app_settings_id = Column(
483 "app_settings_id", Integer(), nullable=False, unique=True,
483 "app_settings_id", Integer(), nullable=False, unique=True,
484 default=None, primary_key=True)
484 default=None, primary_key=True)
485 app_settings_name = Column(
485 app_settings_name = Column(
486 "app_settings_name", String(255), nullable=True, unique=None,
486 "app_settings_name", String(255), nullable=True, unique=None,
487 default=None)
487 default=None)
488 _app_settings_value = Column(
488 _app_settings_value = Column(
489 "app_settings_value", String(4096), nullable=True, unique=None,
489 "app_settings_value", String(4096), nullable=True, unique=None,
490 default=None)
490 default=None)
491 _app_settings_type = Column(
491 _app_settings_type = Column(
492 "app_settings_type", String(255), nullable=True, unique=None,
492 "app_settings_type", String(255), nullable=True, unique=None,
493 default=None)
493 default=None)
494
494
495 repository = relationship('Repository', viewonly=True)
495 repository = relationship('Repository', viewonly=True)
496
496
497 def __init__(self, repository_id, key='', val='', type='unicode'):
497 def __init__(self, repository_id, key='', val='', type='unicode'):
498 self.repository_id = repository_id
498 self.repository_id = repository_id
499 self.app_settings_name = key
499 self.app_settings_name = key
500 self.app_settings_type = type
500 self.app_settings_type = type
501 self.app_settings_value = val
501 self.app_settings_value = val
502
502
503 @validates('_app_settings_value')
503 @validates('_app_settings_value')
504 def validate_settings_value(self, key, val):
504 def validate_settings_value(self, key, val):
505 assert type(val) == str
505 assert type(val) == str
506 return val
506 return val
507
507
508 @hybrid_property
508 @hybrid_property
509 def app_settings_value(self):
509 def app_settings_value(self):
510 v = self._app_settings_value
510 v = self._app_settings_value
511 type_ = self.app_settings_type
511 type_ = self.app_settings_type
512 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
512 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
513 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
513 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
514 return converter(v)
514 return converter(v)
515
515
516 @app_settings_value.setter
516 @app_settings_value.setter
517 def app_settings_value(self, val):
517 def app_settings_value(self, val):
518 """
518 """
519 Setter that will always make sure we use unicode in app_settings_value
519 Setter that will always make sure we use unicode in app_settings_value
520
520
521 :param val:
521 :param val:
522 """
522 """
523 self._app_settings_value = safe_str(val)
523 self._app_settings_value = safe_str(val)
524
524
525 @hybrid_property
525 @hybrid_property
526 def app_settings_type(self):
526 def app_settings_type(self):
527 return self._app_settings_type
527 return self._app_settings_type
528
528
529 @app_settings_type.setter
529 @app_settings_type.setter
530 def app_settings_type(self, val):
530 def app_settings_type(self, val):
531 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
531 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
532 if val not in SETTINGS_TYPES:
532 if val not in SETTINGS_TYPES:
533 raise Exception('type must be one of %s got %s'
533 raise Exception('type must be one of %s got %s'
534 % (SETTINGS_TYPES.keys(), val))
534 % (SETTINGS_TYPES.keys(), val))
535 self._app_settings_type = val
535 self._app_settings_type = val
536
536
537 def __repr__(self):
537 def __repr__(self):
538 return "<%s('%s:%s:%s[%s]')>" % (
538 return "<%s('%s:%s:%s[%s]')>" % (
539 self.cls_name, self.repository.repo_name,
539 self.cls_name, self.repository.repo_name,
540 self.app_settings_name, self.app_settings_value,
540 self.app_settings_name, self.app_settings_value,
541 self.app_settings_type
541 self.app_settings_type
542 )
542 )
543
543
544
544
545 class RepoRhodeCodeUi(Base, BaseModel):
545 class RepoRhodeCodeUi(Base, BaseModel):
546 __tablename__ = 'repo_rhodecode_ui'
546 __tablename__ = 'repo_rhodecode_ui'
547 __table_args__ = (
547 __table_args__ = (
548 UniqueConstraint(
548 UniqueConstraint(
549 'repository_id', 'ui_section', 'ui_key',
549 'repository_id', 'ui_section', 'ui_key',
550 name='uq_repo_rhodecode_ui_repository_id_section_key'),
550 name='uq_repo_rhodecode_ui_repository_id_section_key'),
551 base_table_args
551 base_table_args
552 )
552 )
553
553
554 repository_id = Column(
554 repository_id = Column(
555 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
555 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
556 nullable=False)
556 nullable=False)
557 ui_id = Column(
557 ui_id = Column(
558 "ui_id", Integer(), nullable=False, unique=True, default=None,
558 "ui_id", Integer(), nullable=False, unique=True, default=None,
559 primary_key=True)
559 primary_key=True)
560 ui_section = Column(
560 ui_section = Column(
561 "ui_section", String(255), nullable=True, unique=None, default=None)
561 "ui_section", String(255), nullable=True, unique=None, default=None)
562 ui_key = Column(
562 ui_key = Column(
563 "ui_key", String(255), nullable=True, unique=None, default=None)
563 "ui_key", String(255), nullable=True, unique=None, default=None)
564 ui_value = Column(
564 ui_value = Column(
565 "ui_value", String(255), nullable=True, unique=None, default=None)
565 "ui_value", String(255), nullable=True, unique=None, default=None)
566 ui_active = Column(
566 ui_active = Column(
567 "ui_active", Boolean(), nullable=True, unique=None, default=True)
567 "ui_active", Boolean(), nullable=True, unique=None, default=True)
568
568
569 repository = relationship('Repository', viewonly=True)
569 repository = relationship('Repository', viewonly=True)
570
570
571 def __repr__(self):
571 def __repr__(self):
572 return '<%s[%s:%s]%s=>%s]>' % (
572 return '<%s[%s:%s]%s=>%s]>' % (
573 self.cls_name, self.repository.repo_name,
573 self.cls_name, self.repository.repo_name,
574 self.ui_section, self.ui_key, self.ui_value)
574 self.ui_section, self.ui_key, self.ui_value)
575
575
576
576
577 class User(Base, BaseModel):
577 class User(Base, BaseModel):
578 __tablename__ = 'users'
578 __tablename__ = 'users'
579 __table_args__ = (
579 __table_args__ = (
580 UniqueConstraint('username'), UniqueConstraint('email'),
580 UniqueConstraint('username'), UniqueConstraint('email'),
581 Index('u_username_idx', 'username'),
581 Index('u_username_idx', 'username'),
582 Index('u_email_idx', 'email'),
582 Index('u_email_idx', 'email'),
583 base_table_args
583 base_table_args
584 )
584 )
585
585
586 DEFAULT_USER = 'default'
586 DEFAULT_USER = 'default'
587 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
587 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
588 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
588 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
589
589
590 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
590 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
591 username = Column("username", String(255), nullable=True, unique=None, default=None)
591 username = Column("username", String(255), nullable=True, unique=None, default=None)
592 password = Column("password", String(255), nullable=True, unique=None, default=None)
592 password = Column("password", String(255), nullable=True, unique=None, default=None)
593 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
593 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
594 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
594 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
595 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
595 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
596 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
596 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
597 _email = Column("email", String(255), nullable=True, unique=None, default=None)
597 _email = Column("email", String(255), nullable=True, unique=None, default=None)
598 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
598 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
599 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
599 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
600 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
600 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
601
601
602 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
602 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
603 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
603 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
604 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
604 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
605 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
605 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
606 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
606 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
607 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
607 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
608
608
609 user_log = relationship('UserLog', back_populates='user')
609 user_log = relationship('UserLog', back_populates='user')
610 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
610 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
611
611
612 repositories = relationship('Repository', back_populates='user')
612 repositories = relationship('Repository', back_populates='user')
613 repository_groups = relationship('RepoGroup', back_populates='user')
613 repository_groups = relationship('RepoGroup', back_populates='user')
614 user_groups = relationship('UserGroup', back_populates='user')
614 user_groups = relationship('UserGroup', back_populates='user')
615
615
616 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all', back_populates='follows_user')
616 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all', back_populates='follows_user')
617 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all', back_populates='user')
617 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all', back_populates='user')
618
618
619 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
619 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
620 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
620 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
621 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
621 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
622
622
623 group_member = relationship('UserGroupMember', cascade='all', back_populates='user')
623 group_member = relationship('UserGroupMember', cascade='all', back_populates='user')
624
624
625 notifications = relationship('UserNotification', cascade='all', back_populates='user')
625 notifications = relationship('UserNotification', cascade='all', back_populates='user')
626 # notifications assigned to this user
626 # notifications assigned to this user
627 user_created_notifications = relationship('Notification', cascade='all', back_populates='created_by_user')
627 user_created_notifications = relationship('Notification', cascade='all', back_populates='created_by_user')
628 # comments created by this user
628 # comments created by this user
629 user_comments = relationship('ChangesetComment', cascade='all', back_populates='author')
629 user_comments = relationship('ChangesetComment', cascade='all', back_populates='author')
630 # user profile extra info
630 # user profile extra info
631 user_emails = relationship('UserEmailMap', cascade='all', back_populates='user')
631 user_emails = relationship('UserEmailMap', cascade='all', back_populates='user')
632 user_ip_map = relationship('UserIpMap', cascade='all', back_populates='user')
632 user_ip_map = relationship('UserIpMap', cascade='all', back_populates='user')
633 user_auth_tokens = relationship('UserApiKeys', cascade='all', back_populates='user')
633 user_auth_tokens = relationship('UserApiKeys', cascade='all', back_populates='user')
634 user_ssh_keys = relationship('UserSshKeys', cascade='all', back_populates='user')
634 user_ssh_keys = relationship('UserSshKeys', cascade='all', back_populates='user')
635
635
636 # gists
636 # gists
637 user_gists = relationship('Gist', cascade='all', back_populates='owner')
637 user_gists = relationship('Gist', cascade='all', back_populates='owner')
638 # user pull requests
638 # user pull requests
639 user_pull_requests = relationship('PullRequest', cascade='all', back_populates='author')
639 user_pull_requests = relationship('PullRequest', cascade='all', back_populates='author')
640
640
641 # external identities
641 # external identities
642 external_identities = relationship('ExternalIdentity', primaryjoin="User.user_id==ExternalIdentity.local_user_id", cascade='all')
642 external_identities = relationship('ExternalIdentity', primaryjoin="User.user_id==ExternalIdentity.local_user_id", cascade='all')
643 # review rules
643 # review rules
644 user_review_rules = relationship('RepoReviewRuleUser', cascade='all', back_populates='user')
644 user_review_rules = relationship('RepoReviewRuleUser', cascade='all', back_populates='user')
645
645
646 # artifacts owned
646 # artifacts owned
647 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id', back_populates='upload_user')
647 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id', back_populates='upload_user')
648
648
649 # no cascade, set NULL
649 # no cascade, set NULL
650 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id', cascade='', back_populates='user')
650 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id', cascade='', back_populates='user')
651
651
652 def __repr__(self):
652 def __repr__(self):
653 return f"<{self.cls_name}('id={self.user_id}, username={self.username}')>"
653 return f"<{self.cls_name}('id={self.user_id}, username={self.username}')>"
654
654
655 @hybrid_property
655 @hybrid_property
656 def email(self):
656 def email(self):
657 return self._email
657 return self._email
658
658
659 @email.setter
659 @email.setter
660 def email(self, val):
660 def email(self, val):
661 self._email = val.lower() if val else None
661 self._email = val.lower() if val else None
662
662
663 @hybrid_property
663 @hybrid_property
664 def first_name(self):
664 def first_name(self):
665 from rhodecode.lib import helpers as h
665 from rhodecode.lib import helpers as h
666 if self.name:
666 if self.name:
667 return h.escape(self.name)
667 return h.escape(self.name)
668 return self.name
668 return self.name
669
669
670 @hybrid_property
670 @hybrid_property
671 def last_name(self):
671 def last_name(self):
672 from rhodecode.lib import helpers as h
672 from rhodecode.lib import helpers as h
673 if self.lastname:
673 if self.lastname:
674 return h.escape(self.lastname)
674 return h.escape(self.lastname)
675 return self.lastname
675 return self.lastname
676
676
677 @hybrid_property
677 @hybrid_property
678 def api_key(self):
678 def api_key(self):
679 """
679 """
680 Fetch if exist an auth-token with role ALL connected to this user
680 Fetch if exist an auth-token with role ALL connected to this user
681 """
681 """
682 user_auth_token = UserApiKeys.query()\
682 user_auth_token = UserApiKeys.query()\
683 .filter(UserApiKeys.user_id == self.user_id)\
683 .filter(UserApiKeys.user_id == self.user_id)\
684 .filter(or_(UserApiKeys.expires == -1,
684 .filter(or_(UserApiKeys.expires == -1,
685 UserApiKeys.expires >= time.time()))\
685 UserApiKeys.expires >= time.time()))\
686 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
686 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
687 if user_auth_token:
687 if user_auth_token:
688 user_auth_token = user_auth_token.api_key
688 user_auth_token = user_auth_token.api_key
689
689
690 return user_auth_token
690 return user_auth_token
691
691
692 @api_key.setter
692 @api_key.setter
693 def api_key(self, val):
693 def api_key(self, val):
694 # don't allow to set API key this is deprecated for now
694 # don't allow to set API key this is deprecated for now
695 self._api_key = None
695 self._api_key = None
696
696
697 @property
697 @property
698 def reviewer_pull_requests(self):
698 def reviewer_pull_requests(self):
699 return PullRequestReviewers.query() \
699 return PullRequestReviewers.query() \
700 .options(joinedload(PullRequestReviewers.pull_request)) \
700 .options(joinedload(PullRequestReviewers.pull_request)) \
701 .filter(PullRequestReviewers.user_id == self.user_id) \
701 .filter(PullRequestReviewers.user_id == self.user_id) \
702 .all()
702 .all()
703
703
704 @property
704 @property
705 def firstname(self):
705 def firstname(self):
706 # alias for future
706 # alias for future
707 return self.name
707 return self.name
708
708
709 @property
709 @property
710 def emails(self):
710 def emails(self):
711 other = UserEmailMap.query()\
711 other = UserEmailMap.query()\
712 .filter(UserEmailMap.user == self) \
712 .filter(UserEmailMap.user == self) \
713 .order_by(UserEmailMap.email_id.asc()) \
713 .order_by(UserEmailMap.email_id.asc()) \
714 .all()
714 .all()
715 return [self.email] + [x.email for x in other]
715 return [self.email] + [x.email for x in other]
716
716
717 def emails_cached(self):
717 def emails_cached(self):
718 emails = []
718 emails = []
719 if self.user_id != self.get_default_user_id():
719 if self.user_id != self.get_default_user_id():
720 emails = UserEmailMap.query()\
720 emails = UserEmailMap.query()\
721 .filter(UserEmailMap.user == self) \
721 .filter(UserEmailMap.user == self) \
722 .order_by(UserEmailMap.email_id.asc())
722 .order_by(UserEmailMap.email_id.asc())
723
723
724 emails = emails.options(
724 emails = emails.options(
725 FromCache("sql_cache_short", f"get_user_{self.user_id}_emails")
725 FromCache("sql_cache_short", f"get_user_{self.user_id}_emails")
726 )
726 )
727
727
728 return [self.email] + [x.email for x in emails]
728 return [self.email] + [x.email for x in emails]
729
729
730 @property
730 @property
731 def auth_tokens(self):
731 def auth_tokens(self):
732 auth_tokens = self.get_auth_tokens()
732 auth_tokens = self.get_auth_tokens()
733 return [x.api_key for x in auth_tokens]
733 return [x.api_key for x in auth_tokens]
734
734
735 def get_auth_tokens(self):
735 def get_auth_tokens(self):
736 return UserApiKeys.query()\
736 return UserApiKeys.query()\
737 .filter(UserApiKeys.user == self)\
737 .filter(UserApiKeys.user == self)\
738 .order_by(UserApiKeys.user_api_key_id.asc())\
738 .order_by(UserApiKeys.user_api_key_id.asc())\
739 .all()
739 .all()
740
740
741 @LazyProperty
741 @LazyProperty
742 def feed_token(self):
742 def feed_token(self):
743 return self.get_feed_token()
743 return self.get_feed_token()
744
744
745 def get_feed_token(self, cache=True):
745 def get_feed_token(self, cache=True):
746 feed_tokens = UserApiKeys.query()\
746 feed_tokens = UserApiKeys.query()\
747 .filter(UserApiKeys.user == self)\
747 .filter(UserApiKeys.user == self)\
748 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
748 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
749 if cache:
749 if cache:
750 feed_tokens = feed_tokens.options(
750 feed_tokens = feed_tokens.options(
751 FromCache("sql_cache_short", f"get_user_feed_token_{self.user_id}"))
751 FromCache("sql_cache_short", f"get_user_feed_token_{self.user_id}"))
752
752
753 feed_tokens = feed_tokens.all()
753 feed_tokens = feed_tokens.all()
754 if feed_tokens:
754 if feed_tokens:
755 return feed_tokens[0].api_key
755 return feed_tokens[0].api_key
756 return 'NO_FEED_TOKEN_AVAILABLE'
756 return 'NO_FEED_TOKEN_AVAILABLE'
757
757
758 @LazyProperty
758 @LazyProperty
759 def artifact_token(self):
759 def artifact_token(self):
760 return self.get_artifact_token()
760 return self.get_artifact_token()
761
761
762 def get_artifact_token(self, cache=True):
762 def get_artifact_token(self, cache=True):
763 artifacts_tokens = UserApiKeys.query()\
763 artifacts_tokens = UserApiKeys.query()\
764 .filter(UserApiKeys.user == self) \
764 .filter(UserApiKeys.user == self) \
765 .filter(or_(UserApiKeys.expires == -1,
765 .filter(or_(UserApiKeys.expires == -1,
766 UserApiKeys.expires >= time.time())) \
766 UserApiKeys.expires >= time.time())) \
767 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
767 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
768
768
769 if cache:
769 if cache:
770 artifacts_tokens = artifacts_tokens.options(
770 artifacts_tokens = artifacts_tokens.options(
771 FromCache("sql_cache_short", f"get_user_artifact_token_{self.user_id}"))
771 FromCache("sql_cache_short", f"get_user_artifact_token_{self.user_id}"))
772
772
773 artifacts_tokens = artifacts_tokens.all()
773 artifacts_tokens = artifacts_tokens.all()
774 if artifacts_tokens:
774 if artifacts_tokens:
775 return artifacts_tokens[0].api_key
775 return artifacts_tokens[0].api_key
776 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
776 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
777
777
778 def get_or_create_artifact_token(self):
778 def get_or_create_artifact_token(self):
779 artifacts_tokens = UserApiKeys.query()\
779 artifacts_tokens = UserApiKeys.query()\
780 .filter(UserApiKeys.user == self) \
780 .filter(UserApiKeys.user == self) \
781 .filter(or_(UserApiKeys.expires == -1,
781 .filter(or_(UserApiKeys.expires == -1,
782 UserApiKeys.expires >= time.time())) \
782 UserApiKeys.expires >= time.time())) \
783 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
783 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
784
784
785 artifacts_tokens = artifacts_tokens.all()
785 artifacts_tokens = artifacts_tokens.all()
786 if artifacts_tokens:
786 if artifacts_tokens:
787 return artifacts_tokens[0].api_key
787 return artifacts_tokens[0].api_key
788 else:
788 else:
789 from rhodecode.model.auth_token import AuthTokenModel
789 from rhodecode.model.auth_token import AuthTokenModel
790 artifact_token = AuthTokenModel().create(
790 artifact_token = AuthTokenModel().create(
791 self, 'auto-generated-artifact-token',
791 self, 'auto-generated-artifact-token',
792 lifetime=-1, role=UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
792 lifetime=-1, role=UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
793 Session.commit()
793 Session.commit()
794 return artifact_token.api_key
794 return artifact_token.api_key
795
795
796 @classmethod
796 @classmethod
797 def get(cls, user_id, cache=False):
797 def get(cls, user_id, cache=False):
798 if not user_id:
798 if not user_id:
799 return
799 return
800
800
801 user = cls.query()
801 user = cls.query()
802 if cache:
802 if cache:
803 user = user.options(
803 user = user.options(
804 FromCache("sql_cache_short", f"get_users_{user_id}"))
804 FromCache("sql_cache_short", f"get_users_{user_id}"))
805 return user.get(user_id)
805 return user.get(user_id)
806
806
807 @classmethod
807 @classmethod
808 def extra_valid_auth_tokens(cls, user, role=None):
808 def extra_valid_auth_tokens(cls, user, role=None):
809 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
809 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
810 .filter(or_(UserApiKeys.expires == -1,
810 .filter(or_(UserApiKeys.expires == -1,
811 UserApiKeys.expires >= time.time()))
811 UserApiKeys.expires >= time.time()))
812 if role:
812 if role:
813 tokens = tokens.filter(or_(UserApiKeys.role == role,
813 tokens = tokens.filter(or_(UserApiKeys.role == role,
814 UserApiKeys.role == UserApiKeys.ROLE_ALL))
814 UserApiKeys.role == UserApiKeys.ROLE_ALL))
815 return tokens.all()
815 return tokens.all()
816
816
817 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
817 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
818 from rhodecode.lib import auth
818 from rhodecode.lib import auth
819
819
820 log.debug('Trying to authenticate user: %s via auth-token, '
820 log.debug('Trying to authenticate user: %s via auth-token, '
821 'and roles: %s', self, roles)
821 'and roles: %s', self, roles)
822
822
823 if not auth_token:
823 if not auth_token:
824 return False
824 return False
825
825
826 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
826 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
827 tokens_q = UserApiKeys.query()\
827 tokens_q = UserApiKeys.query()\
828 .filter(UserApiKeys.user_id == self.user_id)\
828 .filter(UserApiKeys.user_id == self.user_id)\
829 .filter(or_(UserApiKeys.expires == -1,
829 .filter(or_(UserApiKeys.expires == -1,
830 UserApiKeys.expires >= time.time()))
830 UserApiKeys.expires >= time.time()))
831
831
832 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
832 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
833
833
834 crypto_backend = auth.crypto_backend()
834 crypto_backend = auth.crypto_backend()
835 enc_token_map = {}
835 enc_token_map = {}
836 plain_token_map = {}
836 plain_token_map = {}
837 for token in tokens_q:
837 for token in tokens_q:
838 if token.api_key.startswith(crypto_backend.ENC_PREF):
838 if token.api_key.startswith(crypto_backend.ENC_PREF):
839 enc_token_map[token.api_key] = token
839 enc_token_map[token.api_key] = token
840 else:
840 else:
841 plain_token_map[token.api_key] = token
841 plain_token_map[token.api_key] = token
842 log.debug(
842 log.debug(
843 'Found %s plain and %s encrypted tokens to check for authentication for this user',
843 'Found %s plain and %s encrypted tokens to check for authentication for this user',
844 len(plain_token_map), len(enc_token_map))
844 len(plain_token_map), len(enc_token_map))
845
845
846 # plain token match comes first
846 # plain token match comes first
847 match = plain_token_map.get(auth_token)
847 match = plain_token_map.get(auth_token)
848
848
849 # check encrypted tokens now
849 # check encrypted tokens now
850 if not match:
850 if not match:
851 for token_hash, token in enc_token_map.items():
851 for token_hash, token in enc_token_map.items():
852 # NOTE(marcink): this is expensive to calculate, but most secure
852 # NOTE(marcink): this is expensive to calculate, but most secure
853 if crypto_backend.hash_check(auth_token, token_hash):
853 if crypto_backend.hash_check(auth_token, token_hash):
854 match = token
854 match = token
855 break
855 break
856
856
857 if match:
857 if match:
858 log.debug('Found matching token %s', match)
858 log.debug('Found matching token %s', match)
859 if match.repo_id:
859 if match.repo_id:
860 log.debug('Found scope, checking for scope match of token %s', match)
860 log.debug('Found scope, checking for scope match of token %s', match)
861 if match.repo_id == scope_repo_id:
861 if match.repo_id == scope_repo_id:
862 return True
862 return True
863 else:
863 else:
864 log.debug(
864 log.debug(
865 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
865 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
866 'and calling scope is:%s, skipping further checks',
866 'and calling scope is:%s, skipping further checks',
867 match.repo, scope_repo_id)
867 match.repo, scope_repo_id)
868 return False
868 return False
869 else:
869 else:
870 return True
870 return True
871
871
872 return False
872 return False
873
873
874 @property
874 @property
875 def ip_addresses(self):
875 def ip_addresses(self):
876 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
876 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
877 return [x.ip_addr for x in ret]
877 return [x.ip_addr for x in ret]
878
878
879 @property
879 @property
880 def username_and_name(self):
880 def username_and_name(self):
881 return f'{self.username} ({self.first_name} {self.last_name})'
881 return f'{self.username} ({self.first_name} {self.last_name})'
882
882
883 @property
883 @property
884 def username_or_name_or_email(self):
884 def username_or_name_or_email(self):
885 full_name = self.full_name if self.full_name != ' ' else None
885 full_name = self.full_name if self.full_name != ' ' else None
886 return self.username or full_name or self.email
886 return self.username or full_name or self.email
887
887
888 @property
888 @property
889 def full_name(self):
889 def full_name(self):
890 return f'{self.first_name} {self.last_name}'
890 return f'{self.first_name} {self.last_name}'
891
891
892 @property
892 @property
893 def full_name_or_username(self):
893 def full_name_or_username(self):
894 return (f'{self.first_name} {self.last_name}'
894 return (f'{self.first_name} {self.last_name}'
895 if (self.first_name and self.last_name) else self.username)
895 if (self.first_name and self.last_name) else self.username)
896
896
897 @property
897 @property
898 def full_contact(self):
898 def full_contact(self):
899 return f'{self.first_name} {self.last_name} <{self.email}>'
899 return f'{self.first_name} {self.last_name} <{self.email}>'
900
900
901 @property
901 @property
902 def short_contact(self):
902 def short_contact(self):
903 return f'{self.first_name} {self.last_name}'
903 return f'{self.first_name} {self.last_name}'
904
904
905 @property
905 @property
906 def is_admin(self):
906 def is_admin(self):
907 return self.admin
907 return self.admin
908
908
909 @property
909 @property
910 def language(self):
910 def language(self):
911 return self.user_data.get('language')
911 return self.user_data.get('language')
912
912
913 def AuthUser(self, **kwargs):
913 def AuthUser(self, **kwargs):
914 """
914 """
915 Returns instance of AuthUser for this user
915 Returns instance of AuthUser for this user
916 """
916 """
917 from rhodecode.lib.auth import AuthUser
917 from rhodecode.lib.auth import AuthUser
918 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
918 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
919
919
920 @hybrid_property
920 @hybrid_property
921 def user_data(self):
921 def user_data(self):
922 if not self._user_data:
922 if not self._user_data:
923 return {}
923 return {}
924
924
925 try:
925 try:
926 return json.loads(self._user_data) or {}
926 return json.loads(self._user_data) or {}
927 except TypeError:
927 except TypeError:
928 return {}
928 return {}
929
929
930 @user_data.setter
930 @user_data.setter
931 def user_data(self, val):
931 def user_data(self, val):
932 if not isinstance(val, dict):
932 if not isinstance(val, dict):
933 raise Exception('user_data must be dict, got %s' % type(val))
933 raise Exception('user_data must be dict, got %s' % type(val))
934 try:
934 try:
935 self._user_data = safe_bytes(json.dumps(val))
935 self._user_data = safe_bytes(json.dumps(val))
936 except Exception:
936 except Exception:
937 log.error(traceback.format_exc())
937 log.error(traceback.format_exc())
938
938
939 @classmethod
939 @classmethod
940 def get_by_username(cls, username, case_insensitive=False,
940 def get_by_username(cls, username, case_insensitive=False,
941 cache=False):
941 cache=False):
942
942
943 if case_insensitive:
943 if case_insensitive:
944 q = cls.select().where(
944 q = cls.select().where(
945 func.lower(cls.username) == func.lower(username))
945 func.lower(cls.username) == func.lower(username))
946 else:
946 else:
947 q = cls.select().where(cls.username == username)
947 q = cls.select().where(cls.username == username)
948
948
949 if cache:
949 if cache:
950 hash_key = _hash_key(username)
950 hash_key = _hash_key(username)
951 q = q.options(
951 q = q.options(
952 FromCache("sql_cache_short", f"get_user_by_name_{hash_key}"))
952 FromCache("sql_cache_short", f"get_user_by_name_{hash_key}"))
953
953
954 return cls.execute(q).scalar_one_or_none()
954 return cls.execute(q).scalar_one_or_none()
955
955
956 @classmethod
956 @classmethod
957 def get_by_username_or_primary_email(cls, user_identifier):
958 qs = union_all(cls.select().where(func.lower(cls.username) == func.lower(user_identifier)),
959 cls.select().where(func.lower(cls.email) == func.lower(user_identifier)))
960 return cls.execute(cls.select(User).from_statement(qs)).scalar_one_or_none()
961
962 @classmethod
957 def get_by_auth_token(cls, auth_token, cache=False):
963 def get_by_auth_token(cls, auth_token, cache=False):
958
964
959 q = cls.select(User)\
965 q = cls.select(User)\
960 .join(UserApiKeys)\
966 .join(UserApiKeys)\
961 .where(UserApiKeys.api_key == auth_token)\
967 .where(UserApiKeys.api_key == auth_token)\
962 .where(or_(UserApiKeys.expires == -1,
968 .where(or_(UserApiKeys.expires == -1,
963 UserApiKeys.expires >= time.time()))
969 UserApiKeys.expires >= time.time()))
964
970
965 if cache:
971 if cache:
966 q = q.options(
972 q = q.options(
967 FromCache("sql_cache_short", f"get_auth_token_{auth_token}"))
973 FromCache("sql_cache_short", f"get_auth_token_{auth_token}"))
968
974
969 matched_user = cls.execute(q).scalar_one_or_none()
975 matched_user = cls.execute(q).scalar_one_or_none()
970
976
971 return matched_user
977 return matched_user
972
978
973 @classmethod
979 @classmethod
974 def get_by_email(cls, email, case_insensitive=False, cache=False):
980 def get_by_email(cls, email, case_insensitive=False, cache=False):
975
981
976 if case_insensitive:
982 if case_insensitive:
977 q = cls.select().where(func.lower(cls.email) == func.lower(email))
983 q = cls.select().where(func.lower(cls.email) == func.lower(email))
978 else:
984 else:
979 q = cls.select().where(cls.email == email)
985 q = cls.select().where(cls.email == email)
980
986
981 if cache:
987 if cache:
982 email_key = _hash_key(email)
988 email_key = _hash_key(email)
983 q = q.options(
989 q = q.options(
984 FromCache("sql_cache_short", f"get_email_key_{email_key}"))
990 FromCache("sql_cache_short", f"get_email_key_{email_key}"))
985
991
986 ret = cls.execute(q).scalar_one_or_none()
992 ret = cls.execute(q).scalar_one_or_none()
987
993
988 if ret is None:
994 if ret is None:
989 q = cls.select(UserEmailMap)
995 q = cls.select(UserEmailMap)
990 # try fetching in alternate email map
996 # try fetching in alternate email map
991 if case_insensitive:
997 if case_insensitive:
992 q = q.where(func.lower(UserEmailMap.email) == func.lower(email))
998 q = q.where(func.lower(UserEmailMap.email) == func.lower(email))
993 else:
999 else:
994 q = q.where(UserEmailMap.email == email)
1000 q = q.where(UserEmailMap.email == email)
995 q = q.options(joinedload(UserEmailMap.user))
1001 q = q.options(joinedload(UserEmailMap.user))
996 if cache:
1002 if cache:
997 q = q.options(
1003 q = q.options(
998 FromCache("sql_cache_short", f"get_email_map_key_{email_key}"))
1004 FromCache("sql_cache_short", f"get_email_map_key_{email_key}"))
999
1005
1000 result = cls.execute(q).scalar_one_or_none()
1006 result = cls.execute(q).scalar_one_or_none()
1001 ret = getattr(result, 'user', None)
1007 ret = getattr(result, 'user', None)
1002
1008
1003 return ret
1009 return ret
1004
1010
1005 @classmethod
1011 @classmethod
1006 def get_from_cs_author(cls, author):
1012 def get_from_cs_author(cls, author):
1007 """
1013 """
1008 Tries to get User objects out of commit author string
1014 Tries to get User objects out of commit author string
1009
1015
1010 :param author:
1016 :param author:
1011 """
1017 """
1012 from rhodecode.lib.helpers import email, author_name
1018 from rhodecode.lib.helpers import email, author_name
1013 # Valid email in the attribute passed, see if they're in the system
1019 # Valid email in the attribute passed, see if they're in the system
1014 _email = email(author)
1020 _email = email(author)
1015 if _email:
1021 if _email:
1016 user = cls.get_by_email(_email, case_insensitive=True)
1022 user = cls.get_by_email(_email, case_insensitive=True)
1017 if user:
1023 if user:
1018 return user
1024 return user
1019 # Maybe we can match by username?
1025 # Maybe we can match by username?
1020 _author = author_name(author)
1026 _author = author_name(author)
1021 user = cls.get_by_username(_author, case_insensitive=True)
1027 user = cls.get_by_username(_author, case_insensitive=True)
1022 if user:
1028 if user:
1023 return user
1029 return user
1024
1030
1025 def update_userdata(self, **kwargs):
1031 def update_userdata(self, **kwargs):
1026 usr = self
1032 usr = self
1027 old = usr.user_data
1033 old = usr.user_data
1028 old.update(**kwargs)
1034 old.update(**kwargs)
1029 usr.user_data = old
1035 usr.user_data = old
1030 Session().add(usr)
1036 Session().add(usr)
1031 log.debug('updated userdata with %s', kwargs)
1037 log.debug('updated userdata with %s', kwargs)
1032
1038
1033 def update_lastlogin(self):
1039 def update_lastlogin(self):
1034 """Update user lastlogin"""
1040 """Update user lastlogin"""
1035 self.last_login = datetime.datetime.now()
1041 self.last_login = datetime.datetime.now()
1036 Session().add(self)
1042 Session().add(self)
1037 log.debug('updated user %s lastlogin', self.username)
1043 log.debug('updated user %s lastlogin', self.username)
1038
1044
1039 def update_password(self, new_password):
1045 def update_password(self, new_password):
1040 from rhodecode.lib.auth import get_crypt_password
1046 from rhodecode.lib.auth import get_crypt_password
1041
1047
1042 self.password = get_crypt_password(new_password)
1048 self.password = get_crypt_password(new_password)
1043 Session().add(self)
1049 Session().add(self)
1044
1050
1045 @classmethod
1051 @classmethod
1046 def get_first_super_admin(cls):
1052 def get_first_super_admin(cls):
1047 stmt = cls.select().where(User.admin == true()).order_by(User.user_id.asc())
1053 stmt = cls.select().where(User.admin == true()).order_by(User.user_id.asc())
1048 user = cls.scalars(stmt).first()
1054 user = cls.scalars(stmt).first()
1049
1055
1050 if user is None:
1056 if user is None:
1051 raise Exception('FATAL: Missing administrative account!')
1057 raise Exception('FATAL: Missing administrative account!')
1052 return user
1058 return user
1053
1059
1054 @classmethod
1060 @classmethod
1055 def get_all_super_admins(cls, only_active=False):
1061 def get_all_super_admins(cls, only_active=False):
1056 """
1062 """
1057 Returns all admin accounts sorted by username
1063 Returns all admin accounts sorted by username
1058 """
1064 """
1059 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1065 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1060 if only_active:
1066 if only_active:
1061 qry = qry.filter(User.active == true())
1067 qry = qry.filter(User.active == true())
1062 return qry.all()
1068 return qry.all()
1063
1069
1064 @classmethod
1070 @classmethod
1065 def get_all_user_ids(cls, only_active=True):
1071 def get_all_user_ids(cls, only_active=True):
1066 """
1072 """
1067 Returns all users IDs
1073 Returns all users IDs
1068 """
1074 """
1069 qry = Session().query(User.user_id)
1075 qry = Session().query(User.user_id)
1070
1076
1071 if only_active:
1077 if only_active:
1072 qry = qry.filter(User.active == true())
1078 qry = qry.filter(User.active == true())
1073 return [x.user_id for x in qry]
1079 return [x.user_id for x in qry]
1074
1080
1075 @classmethod
1081 @classmethod
1076 def get_default_user(cls, cache=False, refresh=False):
1082 def get_default_user(cls, cache=False, refresh=False):
1077 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1083 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1078 if user is None:
1084 if user is None:
1079 raise Exception('FATAL: Missing default account!')
1085 raise Exception('FATAL: Missing default account!')
1080 if refresh:
1086 if refresh:
1081 # The default user might be based on outdated state which
1087 # The default user might be based on outdated state which
1082 # has been loaded from the cache.
1088 # has been loaded from the cache.
1083 # A call to refresh() ensures that the
1089 # A call to refresh() ensures that the
1084 # latest state from the database is used.
1090 # latest state from the database is used.
1085 Session().refresh(user)
1091 Session().refresh(user)
1086
1092
1087 return user
1093 return user
1088
1094
1089 @classmethod
1095 @classmethod
1090 def get_default_user_id(cls):
1096 def get_default_user_id(cls):
1091 import rhodecode
1097 import rhodecode
1092 return rhodecode.CONFIG['default_user_id']
1098 return rhodecode.CONFIG['default_user_id']
1093
1099
1094 def _get_default_perms(self, user, suffix=''):
1100 def _get_default_perms(self, user, suffix=''):
1095 from rhodecode.model.permission import PermissionModel
1101 from rhodecode.model.permission import PermissionModel
1096 return PermissionModel().get_default_perms(user.user_perms, suffix)
1102 return PermissionModel().get_default_perms(user.user_perms, suffix)
1097
1103
1098 def get_default_perms(self, suffix=''):
1104 def get_default_perms(self, suffix=''):
1099 return self._get_default_perms(self, suffix)
1105 return self._get_default_perms(self, suffix)
1100
1106
1101 def get_api_data(self, include_secrets=False, details='full'):
1107 def get_api_data(self, include_secrets=False, details='full'):
1102 """
1108 """
1103 Common function for generating user related data for API
1109 Common function for generating user related data for API
1104
1110
1105 :param include_secrets: By default secrets in the API data will be replaced
1111 :param include_secrets: By default secrets in the API data will be replaced
1106 by a placeholder value to prevent exposing this data by accident. In case
1112 by a placeholder value to prevent exposing this data by accident. In case
1107 this data shall be exposed, set this flag to ``True``.
1113 this data shall be exposed, set this flag to ``True``.
1108
1114
1109 :param details: details can be 'basic|full' basic gives only a subset of
1115 :param details: details can be 'basic|full' basic gives only a subset of
1110 the available user information that includes user_id, name and emails.
1116 the available user information that includes user_id, name and emails.
1111 """
1117 """
1112 user = self
1118 user = self
1113 user_data = self.user_data
1119 user_data = self.user_data
1114 data = {
1120 data = {
1115 'user_id': user.user_id,
1121 'user_id': user.user_id,
1116 'username': user.username,
1122 'username': user.username,
1117 'firstname': user.name,
1123 'firstname': user.name,
1118 'lastname': user.lastname,
1124 'lastname': user.lastname,
1119 'description': user.description,
1125 'description': user.description,
1120 'email': user.email,
1126 'email': user.email,
1121 'emails': user.emails,
1127 'emails': user.emails,
1122 }
1128 }
1123 if details == 'basic':
1129 if details == 'basic':
1124 return data
1130 return data
1125
1131
1126 auth_token_length = 40
1132 auth_token_length = 40
1127 auth_token_replacement = '*' * auth_token_length
1133 auth_token_replacement = '*' * auth_token_length
1128
1134
1129 extras = {
1135 extras = {
1130 'auth_tokens': [auth_token_replacement],
1136 'auth_tokens': [auth_token_replacement],
1131 'active': user.active,
1137 'active': user.active,
1132 'admin': user.admin,
1138 'admin': user.admin,
1133 'extern_type': user.extern_type,
1139 'extern_type': user.extern_type,
1134 'extern_name': user.extern_name,
1140 'extern_name': user.extern_name,
1135 'last_login': user.last_login,
1141 'last_login': user.last_login,
1136 'last_activity': user.last_activity,
1142 'last_activity': user.last_activity,
1137 'ip_addresses': user.ip_addresses,
1143 'ip_addresses': user.ip_addresses,
1138 'language': user_data.get('language')
1144 'language': user_data.get('language')
1139 }
1145 }
1140 data.update(extras)
1146 data.update(extras)
1141
1147
1142 if include_secrets:
1148 if include_secrets:
1143 data['auth_tokens'] = user.auth_tokens
1149 data['auth_tokens'] = user.auth_tokens
1144 return data
1150 return data
1145
1151
1146 def __json__(self):
1152 def __json__(self):
1147 data = {
1153 data = {
1148 'full_name': self.full_name,
1154 'full_name': self.full_name,
1149 'full_name_or_username': self.full_name_or_username,
1155 'full_name_or_username': self.full_name_or_username,
1150 'short_contact': self.short_contact,
1156 'short_contact': self.short_contact,
1151 'full_contact': self.full_contact,
1157 'full_contact': self.full_contact,
1152 }
1158 }
1153 data.update(self.get_api_data())
1159 data.update(self.get_api_data())
1154 return data
1160 return data
1155
1161
1156
1162
1157 class UserApiKeys(Base, BaseModel):
1163 class UserApiKeys(Base, BaseModel):
1158 __tablename__ = 'user_api_keys'
1164 __tablename__ = 'user_api_keys'
1159 __table_args__ = (
1165 __table_args__ = (
1160 Index('uak_api_key_idx', 'api_key'),
1166 Index('uak_api_key_idx', 'api_key'),
1161 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1167 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1162 base_table_args
1168 base_table_args
1163 )
1169 )
1164
1170
1165 # ApiKey role
1171 # ApiKey role
1166 ROLE_ALL = 'token_role_all'
1172 ROLE_ALL = 'token_role_all'
1167 ROLE_VCS = 'token_role_vcs'
1173 ROLE_VCS = 'token_role_vcs'
1168 ROLE_API = 'token_role_api'
1174 ROLE_API = 'token_role_api'
1169 ROLE_HTTP = 'token_role_http'
1175 ROLE_HTTP = 'token_role_http'
1170 ROLE_FEED = 'token_role_feed'
1176 ROLE_FEED = 'token_role_feed'
1171 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1177 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1172 # The last one is ignored in the list as we only
1178 # The last one is ignored in the list as we only
1173 # use it for one action, and cannot be created by users
1179 # use it for one action, and cannot be created by users
1174 ROLE_PASSWORD_RESET = 'token_password_reset'
1180 ROLE_PASSWORD_RESET = 'token_password_reset'
1175
1181
1176 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1182 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1177
1183
1178 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1184 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1179 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1185 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1180 api_key = Column("api_key", String(255), nullable=False, unique=True)
1186 api_key = Column("api_key", String(255), nullable=False, unique=True)
1181 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1187 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1182 expires = Column('expires', Float(53), nullable=False)
1188 expires = Column('expires', Float(53), nullable=False)
1183 role = Column('role', String(255), nullable=True)
1189 role = Column('role', String(255), nullable=True)
1184 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1190 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1185
1191
1186 # scope columns
1192 # scope columns
1187 repo_id = Column(
1193 repo_id = Column(
1188 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1194 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1189 nullable=True, unique=None, default=None)
1195 nullable=True, unique=None, default=None)
1190 repo = relationship('Repository', lazy='joined', back_populates='scoped_tokens')
1196 repo = relationship('Repository', lazy='joined', back_populates='scoped_tokens')
1191
1197
1192 repo_group_id = Column(
1198 repo_group_id = Column(
1193 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1199 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1194 nullable=True, unique=None, default=None)
1200 nullable=True, unique=None, default=None)
1195 repo_group = relationship('RepoGroup', lazy='joined')
1201 repo_group = relationship('RepoGroup', lazy='joined')
1196
1202
1197 user = relationship('User', lazy='joined', back_populates='user_auth_tokens')
1203 user = relationship('User', lazy='joined', back_populates='user_auth_tokens')
1198
1204
1199 def __repr__(self):
1205 def __repr__(self):
1200 return f"<{self.cls_name}('{self.role}')>"
1206 return f"<{self.cls_name}('{self.role}')>"
1201
1207
1202 def __json__(self):
1208 def __json__(self):
1203 data = {
1209 data = {
1204 'auth_token': self.api_key,
1210 'auth_token': self.api_key,
1205 'role': self.role,
1211 'role': self.role,
1206 'scope': self.scope_humanized,
1212 'scope': self.scope_humanized,
1207 'expired': self.expired
1213 'expired': self.expired
1208 }
1214 }
1209 return data
1215 return data
1210
1216
1211 def get_api_data(self, include_secrets=False):
1217 def get_api_data(self, include_secrets=False):
1212 data = self.__json__()
1218 data = self.__json__()
1213 if include_secrets:
1219 if include_secrets:
1214 return data
1220 return data
1215 else:
1221 else:
1216 data['auth_token'] = self.token_obfuscated
1222 data['auth_token'] = self.token_obfuscated
1217 return data
1223 return data
1218
1224
1219 @hybrid_property
1225 @hybrid_property
1220 def description_safe(self):
1226 def description_safe(self):
1221 from rhodecode.lib import helpers as h
1227 from rhodecode.lib import helpers as h
1222 return h.escape(self.description)
1228 return h.escape(self.description)
1223
1229
1224 @property
1230 @property
1225 def expired(self):
1231 def expired(self):
1226 if self.expires == -1:
1232 if self.expires == -1:
1227 return False
1233 return False
1228 return time.time() > self.expires
1234 return time.time() > self.expires
1229
1235
1230 @classmethod
1236 @classmethod
1231 def _get_role_name(cls, role):
1237 def _get_role_name(cls, role):
1232 return {
1238 return {
1233 cls.ROLE_ALL: _('all'),
1239 cls.ROLE_ALL: _('all'),
1234 cls.ROLE_HTTP: _('http/web interface'),
1240 cls.ROLE_HTTP: _('http/web interface'),
1235 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1241 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1236 cls.ROLE_API: _('api calls'),
1242 cls.ROLE_API: _('api calls'),
1237 cls.ROLE_FEED: _('feed access'),
1243 cls.ROLE_FEED: _('feed access'),
1238 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1244 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1239 }.get(role, role)
1245 }.get(role, role)
1240
1246
1241 @classmethod
1247 @classmethod
1242 def _get_role_description(cls, role):
1248 def _get_role_description(cls, role):
1243 return {
1249 return {
1244 cls.ROLE_ALL: _('Token for all actions.'),
1250 cls.ROLE_ALL: _('Token for all actions.'),
1245 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1251 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1246 'login using `api_access_controllers_whitelist` functionality.'),
1252 'login using `api_access_controllers_whitelist` functionality.'),
1247 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1253 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1248 'Requires auth_token authentication plugin to be active. <br/>'
1254 'Requires auth_token authentication plugin to be active. <br/>'
1249 'Such Token should be used then instead of a password to '
1255 'Such Token should be used then instead of a password to '
1250 'interact with a repository, and additionally can be '
1256 'interact with a repository, and additionally can be '
1251 'limited to single repository using repo scope.'),
1257 'limited to single repository using repo scope.'),
1252 cls.ROLE_API: _('Token limited to api calls.'),
1258 cls.ROLE_API: _('Token limited to api calls.'),
1253 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1259 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1254 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1260 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1255 }.get(role, role)
1261 }.get(role, role)
1256
1262
1257 @property
1263 @property
1258 def role_humanized(self):
1264 def role_humanized(self):
1259 return self._get_role_name(self.role)
1265 return self._get_role_name(self.role)
1260
1266
1261 def _get_scope(self):
1267 def _get_scope(self):
1262 if self.repo:
1268 if self.repo:
1263 return 'Repository: {}'.format(self.repo.repo_name)
1269 return 'Repository: {}'.format(self.repo.repo_name)
1264 if self.repo_group:
1270 if self.repo_group:
1265 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1271 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1266 return 'Global'
1272 return 'Global'
1267
1273
1268 @property
1274 @property
1269 def scope_humanized(self):
1275 def scope_humanized(self):
1270 return self._get_scope()
1276 return self._get_scope()
1271
1277
1272 @property
1278 @property
1273 def token_obfuscated(self):
1279 def token_obfuscated(self):
1274 if self.api_key:
1280 if self.api_key:
1275 return self.api_key[:4] + "****"
1281 return self.api_key[:4] + "****"
1276
1282
1277
1283
1278 class UserEmailMap(Base, BaseModel):
1284 class UserEmailMap(Base, BaseModel):
1279 __tablename__ = 'user_email_map'
1285 __tablename__ = 'user_email_map'
1280 __table_args__ = (
1286 __table_args__ = (
1281 Index('uem_email_idx', 'email'),
1287 Index('uem_email_idx', 'email'),
1282 Index('uem_user_id_idx', 'user_id'),
1288 Index('uem_user_id_idx', 'user_id'),
1283 UniqueConstraint('email'),
1289 UniqueConstraint('email'),
1284 base_table_args
1290 base_table_args
1285 )
1291 )
1286
1292
1287 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1293 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1288 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1294 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1289 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1295 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1290 user = relationship('User', lazy='joined', back_populates='user_emails')
1296 user = relationship('User', lazy='joined', back_populates='user_emails')
1291
1297
1292 @validates('_email')
1298 @validates('_email')
1293 def validate_email(self, key, email):
1299 def validate_email(self, key, email):
1294 # check if this email is not main one
1300 # check if this email is not main one
1295 main_email = Session().query(User).filter(User.email == email).scalar()
1301 main_email = Session().query(User).filter(User.email == email).scalar()
1296 if main_email is not None:
1302 if main_email is not None:
1297 raise AttributeError('email %s is present is user table' % email)
1303 raise AttributeError('email %s is present is user table' % email)
1298 return email
1304 return email
1299
1305
1300 @hybrid_property
1306 @hybrid_property
1301 def email(self):
1307 def email(self):
1302 return self._email
1308 return self._email
1303
1309
1304 @email.setter
1310 @email.setter
1305 def email(self, val):
1311 def email(self, val):
1306 self._email = val.lower() if val else None
1312 self._email = val.lower() if val else None
1307
1313
1308
1314
1309 class UserIpMap(Base, BaseModel):
1315 class UserIpMap(Base, BaseModel):
1310 __tablename__ = 'user_ip_map'
1316 __tablename__ = 'user_ip_map'
1311 __table_args__ = (
1317 __table_args__ = (
1312 UniqueConstraint('user_id', 'ip_addr'),
1318 UniqueConstraint('user_id', 'ip_addr'),
1313 base_table_args
1319 base_table_args
1314 )
1320 )
1315
1321
1316 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1322 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1317 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1323 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1318 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1324 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1319 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1325 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1320 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1326 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1321 user = relationship('User', lazy='joined', back_populates='user_ip_map')
1327 user = relationship('User', lazy='joined', back_populates='user_ip_map')
1322
1328
1323 @hybrid_property
1329 @hybrid_property
1324 def description_safe(self):
1330 def description_safe(self):
1325 from rhodecode.lib import helpers as h
1331 from rhodecode.lib import helpers as h
1326 return h.escape(self.description)
1332 return h.escape(self.description)
1327
1333
1328 @classmethod
1334 @classmethod
1329 def _get_ip_range(cls, ip_addr):
1335 def _get_ip_range(cls, ip_addr):
1330 net = ipaddress.ip_network(safe_str(ip_addr), strict=False)
1336 net = ipaddress.ip_network(safe_str(ip_addr), strict=False)
1331 return [str(net.network_address), str(net.broadcast_address)]
1337 return [str(net.network_address), str(net.broadcast_address)]
1332
1338
1333 def __json__(self):
1339 def __json__(self):
1334 return {
1340 return {
1335 'ip_addr': self.ip_addr,
1341 'ip_addr': self.ip_addr,
1336 'ip_range': self._get_ip_range(self.ip_addr),
1342 'ip_range': self._get_ip_range(self.ip_addr),
1337 }
1343 }
1338
1344
1339 def __repr__(self):
1345 def __repr__(self):
1340 return f"<{self.cls_name}('user_id={self.user_id} => ip={self.ip_addr}')>"
1346 return f"<{self.cls_name}('user_id={self.user_id} => ip={self.ip_addr}')>"
1341
1347
1342
1348
1343 class UserSshKeys(Base, BaseModel):
1349 class UserSshKeys(Base, BaseModel):
1344 __tablename__ = 'user_ssh_keys'
1350 __tablename__ = 'user_ssh_keys'
1345 __table_args__ = (
1351 __table_args__ = (
1346 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1352 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1347
1353
1348 UniqueConstraint('ssh_key_fingerprint'),
1354 UniqueConstraint('ssh_key_fingerprint'),
1349
1355
1350 base_table_args
1356 base_table_args
1351 )
1357 )
1352
1358
1353 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1359 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1354 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1360 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1355 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1361 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1356
1362
1357 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1363 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1358
1364
1359 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1365 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1360 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1366 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1361 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1367 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1362
1368
1363 user = relationship('User', lazy='joined', back_populates='user_ssh_keys')
1369 user = relationship('User', lazy='joined', back_populates='user_ssh_keys')
1364
1370
1365 def __json__(self):
1371 def __json__(self):
1366 data = {
1372 data = {
1367 'ssh_fingerprint': self.ssh_key_fingerprint,
1373 'ssh_fingerprint': self.ssh_key_fingerprint,
1368 'description': self.description,
1374 'description': self.description,
1369 'created_on': self.created_on
1375 'created_on': self.created_on
1370 }
1376 }
1371 return data
1377 return data
1372
1378
1373 def get_api_data(self):
1379 def get_api_data(self):
1374 data = self.__json__()
1380 data = self.__json__()
1375 return data
1381 return data
1376
1382
1377
1383
1378 class UserLog(Base, BaseModel):
1384 class UserLog(Base, BaseModel):
1379 __tablename__ = 'user_logs'
1385 __tablename__ = 'user_logs'
1380 __table_args__ = (
1386 __table_args__ = (
1381 base_table_args,
1387 base_table_args,
1382 )
1388 )
1383
1389
1384 VERSION_1 = 'v1'
1390 VERSION_1 = 'v1'
1385 VERSION_2 = 'v2'
1391 VERSION_2 = 'v2'
1386 VERSIONS = [VERSION_1, VERSION_2]
1392 VERSIONS = [VERSION_1, VERSION_2]
1387
1393
1388 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1394 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1389 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1395 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1390 username = Column("username", String(255), nullable=True, unique=None, default=None)
1396 username = Column("username", String(255), nullable=True, unique=None, default=None)
1391 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1397 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1392 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1398 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1393 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1399 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1394 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1400 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1395 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1401 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1396
1402
1397 version = Column("version", String(255), nullable=True, default=VERSION_1)
1403 version = Column("version", String(255), nullable=True, default=VERSION_1)
1398 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1404 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1399 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1405 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1400 user = relationship('User', cascade='', back_populates='user_log')
1406 user = relationship('User', cascade='', back_populates='user_log')
1401 repository = relationship('Repository', cascade='', back_populates='logs')
1407 repository = relationship('Repository', cascade='', back_populates='logs')
1402
1408
1403 def __repr__(self):
1409 def __repr__(self):
1404 return f"<{self.cls_name}('id:{self.repository_name}:{self.action}')>"
1410 return f"<{self.cls_name}('id:{self.repository_name}:{self.action}')>"
1405
1411
1406 def __json__(self):
1412 def __json__(self):
1407 return {
1413 return {
1408 'user_id': self.user_id,
1414 'user_id': self.user_id,
1409 'username': self.username,
1415 'username': self.username,
1410 'repository_id': self.repository_id,
1416 'repository_id': self.repository_id,
1411 'repository_name': self.repository_name,
1417 'repository_name': self.repository_name,
1412 'user_ip': self.user_ip,
1418 'user_ip': self.user_ip,
1413 'action_date': self.action_date,
1419 'action_date': self.action_date,
1414 'action': self.action,
1420 'action': self.action,
1415 }
1421 }
1416
1422
1417 @hybrid_property
1423 @hybrid_property
1418 def entry_id(self):
1424 def entry_id(self):
1419 return self.user_log_id
1425 return self.user_log_id
1420
1426
1421 @property
1427 @property
1422 def action_as_day(self):
1428 def action_as_day(self):
1423 return datetime.date(*self.action_date.timetuple()[:3])
1429 return datetime.date(*self.action_date.timetuple()[:3])
1424
1430
1425
1431
1426 class UserGroup(Base, BaseModel):
1432 class UserGroup(Base, BaseModel):
1427 __tablename__ = 'users_groups'
1433 __tablename__ = 'users_groups'
1428 __table_args__ = (
1434 __table_args__ = (
1429 base_table_args,
1435 base_table_args,
1430 )
1436 )
1431
1437
1432 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1438 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1433 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1439 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1434 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1440 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1435 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1441 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1436 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1442 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1437 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1443 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1438 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1444 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1439 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1445 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1440
1446
1441 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined", back_populates='users_group')
1447 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined", back_populates='users_group')
1442 users_group_to_perm = relationship('UserGroupToPerm', cascade='all', back_populates='users_group')
1448 users_group_to_perm = relationship('UserGroupToPerm', cascade='all', back_populates='users_group')
1443 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='users_group')
1449 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='users_group')
1444 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='users_group')
1450 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='users_group')
1445 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all', back_populates='user_group')
1451 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all', back_populates='user_group')
1446
1452
1447 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all', back_populates='target_user_group')
1453 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all', back_populates='target_user_group')
1448
1454
1449 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all', back_populates='users_group')
1455 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all', back_populates='users_group')
1450 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id", back_populates='user_groups')
1456 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id", back_populates='user_groups')
1451
1457
1452 @classmethod
1458 @classmethod
1453 def _load_group_data(cls, column):
1459 def _load_group_data(cls, column):
1454 if not column:
1460 if not column:
1455 return {}
1461 return {}
1456
1462
1457 try:
1463 try:
1458 return json.loads(column) or {}
1464 return json.loads(column) or {}
1459 except TypeError:
1465 except TypeError:
1460 return {}
1466 return {}
1461
1467
1462 @hybrid_property
1468 @hybrid_property
1463 def description_safe(self):
1469 def description_safe(self):
1464 from rhodecode.lib import helpers as h
1470 from rhodecode.lib import helpers as h
1465 return h.escape(self.user_group_description)
1471 return h.escape(self.user_group_description)
1466
1472
1467 @hybrid_property
1473 @hybrid_property
1468 def group_data(self):
1474 def group_data(self):
1469 return self._load_group_data(self._group_data)
1475 return self._load_group_data(self._group_data)
1470
1476
1471 @group_data.expression
1477 @group_data.expression
1472 def group_data(self, **kwargs):
1478 def group_data(self, **kwargs):
1473 return self._group_data
1479 return self._group_data
1474
1480
1475 @group_data.setter
1481 @group_data.setter
1476 def group_data(self, val):
1482 def group_data(self, val):
1477 try:
1483 try:
1478 self._group_data = json.dumps(val)
1484 self._group_data = json.dumps(val)
1479 except Exception:
1485 except Exception:
1480 log.error(traceback.format_exc())
1486 log.error(traceback.format_exc())
1481
1487
1482 @classmethod
1488 @classmethod
1483 def _load_sync(cls, group_data):
1489 def _load_sync(cls, group_data):
1484 if group_data:
1490 if group_data:
1485 return group_data.get('extern_type')
1491 return group_data.get('extern_type')
1486
1492
1487 @property
1493 @property
1488 def sync(self):
1494 def sync(self):
1489 return self._load_sync(self.group_data)
1495 return self._load_sync(self.group_data)
1490
1496
1491 def __repr__(self):
1497 def __repr__(self):
1492 return f"<{self.cls_name}('id:{self.users_group_id}:{self.users_group_name}')>"
1498 return f"<{self.cls_name}('id:{self.users_group_id}:{self.users_group_name}')>"
1493
1499
1494 @classmethod
1500 @classmethod
1495 def get_by_group_name(cls, group_name, cache=False,
1501 def get_by_group_name(cls, group_name, cache=False,
1496 case_insensitive=False):
1502 case_insensitive=False):
1497 if case_insensitive:
1503 if case_insensitive:
1498 q = cls.query().filter(func.lower(cls.users_group_name) ==
1504 q = cls.query().filter(func.lower(cls.users_group_name) ==
1499 func.lower(group_name))
1505 func.lower(group_name))
1500
1506
1501 else:
1507 else:
1502 q = cls.query().filter(cls.users_group_name == group_name)
1508 q = cls.query().filter(cls.users_group_name == group_name)
1503 if cache:
1509 if cache:
1504 name_key = _hash_key(group_name)
1510 name_key = _hash_key(group_name)
1505 q = q.options(
1511 q = q.options(
1506 FromCache("sql_cache_short", f"get_group_{name_key}"))
1512 FromCache("sql_cache_short", f"get_group_{name_key}"))
1507 return q.scalar()
1513 return q.scalar()
1508
1514
1509 @classmethod
1515 @classmethod
1510 def get(cls, user_group_id, cache=False):
1516 def get(cls, user_group_id, cache=False):
1511 if not user_group_id:
1517 if not user_group_id:
1512 return
1518 return
1513
1519
1514 user_group = cls.query()
1520 user_group = cls.query()
1515 if cache:
1521 if cache:
1516 user_group = user_group.options(
1522 user_group = user_group.options(
1517 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1523 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1518 return user_group.get(user_group_id)
1524 return user_group.get(user_group_id)
1519
1525
1520 def permissions(self, with_admins=True, with_owner=True,
1526 def permissions(self, with_admins=True, with_owner=True,
1521 expand_from_user_groups=False):
1527 expand_from_user_groups=False):
1522 """
1528 """
1523 Permissions for user groups
1529 Permissions for user groups
1524 """
1530 """
1525 _admin_perm = 'usergroup.admin'
1531 _admin_perm = 'usergroup.admin'
1526
1532
1527 owner_row = []
1533 owner_row = []
1528 if with_owner:
1534 if with_owner:
1529 usr = AttributeDict(self.user.get_dict())
1535 usr = AttributeDict(self.user.get_dict())
1530 usr.owner_row = True
1536 usr.owner_row = True
1531 usr.permission = _admin_perm
1537 usr.permission = _admin_perm
1532 owner_row.append(usr)
1538 owner_row.append(usr)
1533
1539
1534 super_admin_ids = []
1540 super_admin_ids = []
1535 super_admin_rows = []
1541 super_admin_rows = []
1536 if with_admins:
1542 if with_admins:
1537 for usr in User.get_all_super_admins():
1543 for usr in User.get_all_super_admins():
1538 super_admin_ids.append(usr.user_id)
1544 super_admin_ids.append(usr.user_id)
1539 # if this admin is also owner, don't double the record
1545 # if this admin is also owner, don't double the record
1540 if usr.user_id == owner_row[0].user_id:
1546 if usr.user_id == owner_row[0].user_id:
1541 owner_row[0].admin_row = True
1547 owner_row[0].admin_row = True
1542 else:
1548 else:
1543 usr = AttributeDict(usr.get_dict())
1549 usr = AttributeDict(usr.get_dict())
1544 usr.admin_row = True
1550 usr.admin_row = True
1545 usr.permission = _admin_perm
1551 usr.permission = _admin_perm
1546 super_admin_rows.append(usr)
1552 super_admin_rows.append(usr)
1547
1553
1548 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1554 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1549 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1555 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1550 joinedload(UserUserGroupToPerm.user),
1556 joinedload(UserUserGroupToPerm.user),
1551 joinedload(UserUserGroupToPerm.permission),)
1557 joinedload(UserUserGroupToPerm.permission),)
1552
1558
1553 # get owners and admins and permissions. We do a trick of re-writing
1559 # get owners and admins and permissions. We do a trick of re-writing
1554 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1560 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1555 # has a global reference and changing one object propagates to all
1561 # has a global reference and changing one object propagates to all
1556 # others. This means if admin is also an owner admin_row that change
1562 # others. This means if admin is also an owner admin_row that change
1557 # would propagate to both objects
1563 # would propagate to both objects
1558 perm_rows = []
1564 perm_rows = []
1559 for _usr in q.all():
1565 for _usr in q.all():
1560 usr = AttributeDict(_usr.user.get_dict())
1566 usr = AttributeDict(_usr.user.get_dict())
1561 # if this user is also owner/admin, mark as duplicate record
1567 # if this user is also owner/admin, mark as duplicate record
1562 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1568 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1563 usr.duplicate_perm = True
1569 usr.duplicate_perm = True
1564 usr.permission = _usr.permission.permission_name
1570 usr.permission = _usr.permission.permission_name
1565 perm_rows.append(usr)
1571 perm_rows.append(usr)
1566
1572
1567 # filter the perm rows by 'default' first and then sort them by
1573 # filter the perm rows by 'default' first and then sort them by
1568 # admin,write,read,none permissions sorted again alphabetically in
1574 # admin,write,read,none permissions sorted again alphabetically in
1569 # each group
1575 # each group
1570 perm_rows = sorted(perm_rows, key=display_user_sort)
1576 perm_rows = sorted(perm_rows, key=display_user_sort)
1571
1577
1572 user_groups_rows = []
1578 user_groups_rows = []
1573 if expand_from_user_groups:
1579 if expand_from_user_groups:
1574 for ug in self.permission_user_groups(with_members=True):
1580 for ug in self.permission_user_groups(with_members=True):
1575 for user_data in ug.members:
1581 for user_data in ug.members:
1576 user_groups_rows.append(user_data)
1582 user_groups_rows.append(user_data)
1577
1583
1578 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1584 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1579
1585
1580 def permission_user_groups(self, with_members=False):
1586 def permission_user_groups(self, with_members=False):
1581 q = UserGroupUserGroupToPerm.query()\
1587 q = UserGroupUserGroupToPerm.query()\
1582 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1588 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1583 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1589 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1584 joinedload(UserGroupUserGroupToPerm.target_user_group),
1590 joinedload(UserGroupUserGroupToPerm.target_user_group),
1585 joinedload(UserGroupUserGroupToPerm.permission),)
1591 joinedload(UserGroupUserGroupToPerm.permission),)
1586
1592
1587 perm_rows = []
1593 perm_rows = []
1588 for _user_group in q.all():
1594 for _user_group in q.all():
1589 entry = AttributeDict(_user_group.user_group.get_dict())
1595 entry = AttributeDict(_user_group.user_group.get_dict())
1590 entry.permission = _user_group.permission.permission_name
1596 entry.permission = _user_group.permission.permission_name
1591 if with_members:
1597 if with_members:
1592 entry.members = [x.user.get_dict()
1598 entry.members = [x.user.get_dict()
1593 for x in _user_group.user_group.members]
1599 for x in _user_group.user_group.members]
1594 perm_rows.append(entry)
1600 perm_rows.append(entry)
1595
1601
1596 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1602 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1597 return perm_rows
1603 return perm_rows
1598
1604
1599 def _get_default_perms(self, user_group, suffix=''):
1605 def _get_default_perms(self, user_group, suffix=''):
1600 from rhodecode.model.permission import PermissionModel
1606 from rhodecode.model.permission import PermissionModel
1601 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1607 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1602
1608
1603 def get_default_perms(self, suffix=''):
1609 def get_default_perms(self, suffix=''):
1604 return self._get_default_perms(self, suffix)
1610 return self._get_default_perms(self, suffix)
1605
1611
1606 def get_api_data(self, with_group_members=True, include_secrets=False):
1612 def get_api_data(self, with_group_members=True, include_secrets=False):
1607 """
1613 """
1608 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1614 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1609 basically forwarded.
1615 basically forwarded.
1610
1616
1611 """
1617 """
1612 user_group = self
1618 user_group = self
1613 data = {
1619 data = {
1614 'users_group_id': user_group.users_group_id,
1620 'users_group_id': user_group.users_group_id,
1615 'group_name': user_group.users_group_name,
1621 'group_name': user_group.users_group_name,
1616 'group_description': user_group.user_group_description,
1622 'group_description': user_group.user_group_description,
1617 'active': user_group.users_group_active,
1623 'active': user_group.users_group_active,
1618 'owner': user_group.user.username,
1624 'owner': user_group.user.username,
1619 'sync': user_group.sync,
1625 'sync': user_group.sync,
1620 'owner_email': user_group.user.email,
1626 'owner_email': user_group.user.email,
1621 }
1627 }
1622
1628
1623 if with_group_members:
1629 if with_group_members:
1624 users = []
1630 users = []
1625 for user in user_group.members:
1631 for user in user_group.members:
1626 user = user.user
1632 user = user.user
1627 users.append(user.get_api_data(include_secrets=include_secrets))
1633 users.append(user.get_api_data(include_secrets=include_secrets))
1628 data['users'] = users
1634 data['users'] = users
1629
1635
1630 return data
1636 return data
1631
1637
1632
1638
1633 class UserGroupMember(Base, BaseModel):
1639 class UserGroupMember(Base, BaseModel):
1634 __tablename__ = 'users_groups_members'
1640 __tablename__ = 'users_groups_members'
1635 __table_args__ = (
1641 __table_args__ = (
1636 base_table_args,
1642 base_table_args,
1637 )
1643 )
1638
1644
1639 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1645 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1640 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1646 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1641 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1647 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1642
1648
1643 user = relationship('User', lazy='joined', back_populates='group_member')
1649 user = relationship('User', lazy='joined', back_populates='group_member')
1644 users_group = relationship('UserGroup', back_populates='members')
1650 users_group = relationship('UserGroup', back_populates='members')
1645
1651
1646 def __init__(self, gr_id='', u_id=''):
1652 def __init__(self, gr_id='', u_id=''):
1647 self.users_group_id = gr_id
1653 self.users_group_id = gr_id
1648 self.user_id = u_id
1654 self.user_id = u_id
1649
1655
1650
1656
1651 class RepositoryField(Base, BaseModel):
1657 class RepositoryField(Base, BaseModel):
1652 __tablename__ = 'repositories_fields'
1658 __tablename__ = 'repositories_fields'
1653 __table_args__ = (
1659 __table_args__ = (
1654 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1660 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1655 base_table_args,
1661 base_table_args,
1656 )
1662 )
1657
1663
1658 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1664 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1659
1665
1660 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1666 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1661 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1667 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1662 field_key = Column("field_key", String(250))
1668 field_key = Column("field_key", String(250))
1663 field_label = Column("field_label", String(1024), nullable=False)
1669 field_label = Column("field_label", String(1024), nullable=False)
1664 field_value = Column("field_value", String(10000), nullable=False)
1670 field_value = Column("field_value", String(10000), nullable=False)
1665 field_desc = Column("field_desc", String(1024), nullable=False)
1671 field_desc = Column("field_desc", String(1024), nullable=False)
1666 field_type = Column("field_type", String(255), nullable=False, unique=None)
1672 field_type = Column("field_type", String(255), nullable=False, unique=None)
1667 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1673 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1668
1674
1669 repository = relationship('Repository', back_populates='extra_fields')
1675 repository = relationship('Repository', back_populates='extra_fields')
1670
1676
1671 @property
1677 @property
1672 def field_key_prefixed(self):
1678 def field_key_prefixed(self):
1673 return 'ex_%s' % self.field_key
1679 return 'ex_%s' % self.field_key
1674
1680
1675 @classmethod
1681 @classmethod
1676 def un_prefix_key(cls, key):
1682 def un_prefix_key(cls, key):
1677 if key.startswith(cls.PREFIX):
1683 if key.startswith(cls.PREFIX):
1678 return key[len(cls.PREFIX):]
1684 return key[len(cls.PREFIX):]
1679 return key
1685 return key
1680
1686
1681 @classmethod
1687 @classmethod
1682 def get_by_key_name(cls, key, repo):
1688 def get_by_key_name(cls, key, repo):
1683 row = cls.query()\
1689 row = cls.query()\
1684 .filter(cls.repository == repo)\
1690 .filter(cls.repository == repo)\
1685 .filter(cls.field_key == key).scalar()
1691 .filter(cls.field_key == key).scalar()
1686 return row
1692 return row
1687
1693
1688
1694
1689 class Repository(Base, BaseModel):
1695 class Repository(Base, BaseModel):
1690 __tablename__ = 'repositories'
1696 __tablename__ = 'repositories'
1691 __table_args__ = (
1697 __table_args__ = (
1692 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1698 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1693 base_table_args,
1699 base_table_args,
1694 )
1700 )
1695 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1701 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1696 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1702 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1697 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1703 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1698
1704
1699 STATE_CREATED = 'repo_state_created'
1705 STATE_CREATED = 'repo_state_created'
1700 STATE_PENDING = 'repo_state_pending'
1706 STATE_PENDING = 'repo_state_pending'
1701 STATE_ERROR = 'repo_state_error'
1707 STATE_ERROR = 'repo_state_error'
1702
1708
1703 LOCK_AUTOMATIC = 'lock_auto'
1709 LOCK_AUTOMATIC = 'lock_auto'
1704 LOCK_API = 'lock_api'
1710 LOCK_API = 'lock_api'
1705 LOCK_WEB = 'lock_web'
1711 LOCK_WEB = 'lock_web'
1706 LOCK_PULL = 'lock_pull'
1712 LOCK_PULL = 'lock_pull'
1707
1713
1708 NAME_SEP = URL_SEP
1714 NAME_SEP = URL_SEP
1709
1715
1710 repo_id = Column(
1716 repo_id = Column(
1711 "repo_id", Integer(), nullable=False, unique=True, default=None,
1717 "repo_id", Integer(), nullable=False, unique=True, default=None,
1712 primary_key=True)
1718 primary_key=True)
1713 _repo_name = Column(
1719 _repo_name = Column(
1714 "repo_name", Text(), nullable=False, default=None)
1720 "repo_name", Text(), nullable=False, default=None)
1715 repo_name_hash = Column(
1721 repo_name_hash = Column(
1716 "repo_name_hash", String(255), nullable=False, unique=True)
1722 "repo_name_hash", String(255), nullable=False, unique=True)
1717 repo_state = Column("repo_state", String(255), nullable=True)
1723 repo_state = Column("repo_state", String(255), nullable=True)
1718
1724
1719 clone_uri = Column(
1725 clone_uri = Column(
1720 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1726 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1721 default=None)
1727 default=None)
1722 push_uri = Column(
1728 push_uri = Column(
1723 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1729 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1724 default=None)
1730 default=None)
1725 repo_type = Column(
1731 repo_type = Column(
1726 "repo_type", String(255), nullable=False, unique=False, default=None)
1732 "repo_type", String(255), nullable=False, unique=False, default=None)
1727 user_id = Column(
1733 user_id = Column(
1728 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1734 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1729 unique=False, default=None)
1735 unique=False, default=None)
1730 private = Column(
1736 private = Column(
1731 "private", Boolean(), nullable=True, unique=None, default=None)
1737 "private", Boolean(), nullable=True, unique=None, default=None)
1732 archived = Column(
1738 archived = Column(
1733 "archived", Boolean(), nullable=True, unique=None, default=None)
1739 "archived", Boolean(), nullable=True, unique=None, default=None)
1734 enable_statistics = Column(
1740 enable_statistics = Column(
1735 "statistics", Boolean(), nullable=True, unique=None, default=True)
1741 "statistics", Boolean(), nullable=True, unique=None, default=True)
1736 enable_downloads = Column(
1742 enable_downloads = Column(
1737 "downloads", Boolean(), nullable=True, unique=None, default=True)
1743 "downloads", Boolean(), nullable=True, unique=None, default=True)
1738 description = Column(
1744 description = Column(
1739 "description", String(10000), nullable=True, unique=None, default=None)
1745 "description", String(10000), nullable=True, unique=None, default=None)
1740 created_on = Column(
1746 created_on = Column(
1741 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1747 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1742 default=datetime.datetime.now)
1748 default=datetime.datetime.now)
1743 updated_on = Column(
1749 updated_on = Column(
1744 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1750 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1745 default=datetime.datetime.now)
1751 default=datetime.datetime.now)
1746 _landing_revision = Column(
1752 _landing_revision = Column(
1747 "landing_revision", String(255), nullable=False, unique=False,
1753 "landing_revision", String(255), nullable=False, unique=False,
1748 default=None)
1754 default=None)
1749 enable_locking = Column(
1755 enable_locking = Column(
1750 "enable_locking", Boolean(), nullable=False, unique=None,
1756 "enable_locking", Boolean(), nullable=False, unique=None,
1751 default=False)
1757 default=False)
1752 _locked = Column(
1758 _locked = Column(
1753 "locked", String(255), nullable=True, unique=False, default=None)
1759 "locked", String(255), nullable=True, unique=False, default=None)
1754 _changeset_cache = Column(
1760 _changeset_cache = Column(
1755 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1761 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1756
1762
1757 fork_id = Column(
1763 fork_id = Column(
1758 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1764 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1759 nullable=True, unique=False, default=None)
1765 nullable=True, unique=False, default=None)
1760 group_id = Column(
1766 group_id = Column(
1761 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1767 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1762 unique=False, default=None)
1768 unique=False, default=None)
1763
1769
1764 user = relationship('User', lazy='joined', back_populates='repositories')
1770 user = relationship('User', lazy='joined', back_populates='repositories')
1765 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1771 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1766 group = relationship('RepoGroup', lazy='joined')
1772 group = relationship('RepoGroup', lazy='joined')
1767 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
1773 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
1768 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='repository')
1774 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='repository')
1769 stats = relationship('Statistics', cascade='all', uselist=False)
1775 stats = relationship('Statistics', cascade='all', uselist=False)
1770
1776
1771 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all', back_populates='follows_repository')
1777 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all', back_populates='follows_repository')
1772 extra_fields = relationship('RepositoryField', cascade="all, delete-orphan", back_populates='repository')
1778 extra_fields = relationship('RepositoryField', cascade="all, delete-orphan", back_populates='repository')
1773
1779
1774 logs = relationship('UserLog', back_populates='repository')
1780 logs = relationship('UserLog', back_populates='repository')
1775
1781
1776 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='repo')
1782 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='repo')
1777
1783
1778 pull_requests_source = relationship(
1784 pull_requests_source = relationship(
1779 'PullRequest',
1785 'PullRequest',
1780 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1786 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1781 cascade="all, delete-orphan",
1787 cascade="all, delete-orphan",
1782 overlaps="source_repo"
1788 overlaps="source_repo"
1783 )
1789 )
1784 pull_requests_target = relationship(
1790 pull_requests_target = relationship(
1785 'PullRequest',
1791 'PullRequest',
1786 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1792 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1787 cascade="all, delete-orphan",
1793 cascade="all, delete-orphan",
1788 overlaps="target_repo"
1794 overlaps="target_repo"
1789 )
1795 )
1790
1796
1791 ui = relationship('RepoRhodeCodeUi', cascade="all")
1797 ui = relationship('RepoRhodeCodeUi', cascade="all")
1792 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1798 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1793 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo')
1799 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo')
1794
1800
1795 scoped_tokens = relationship('UserApiKeys', cascade="all", back_populates='repo')
1801 scoped_tokens = relationship('UserApiKeys', cascade="all", back_populates='repo')
1796
1802
1797 # no cascade, set NULL
1803 # no cascade, set NULL
1798 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id', viewonly=True)
1804 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id', viewonly=True)
1799
1805
1800 review_rules = relationship('RepoReviewRule')
1806 review_rules = relationship('RepoReviewRule')
1801 user_branch_perms = relationship('UserToRepoBranchPermission')
1807 user_branch_perms = relationship('UserToRepoBranchPermission')
1802 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission')
1808 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission')
1803
1809
1804 def __repr__(self):
1810 def __repr__(self):
1805 return "<%s('%s:%s')>" % (self.cls_name, self.repo_id, self.repo_name)
1811 return "<%s('%s:%s')>" % (self.cls_name, self.repo_id, self.repo_name)
1806
1812
1807 @hybrid_property
1813 @hybrid_property
1808 def description_safe(self):
1814 def description_safe(self):
1809 from rhodecode.lib import helpers as h
1815 from rhodecode.lib import helpers as h
1810 return h.escape(self.description)
1816 return h.escape(self.description)
1811
1817
1812 @hybrid_property
1818 @hybrid_property
1813 def landing_rev(self):
1819 def landing_rev(self):
1814 # always should return [rev_type, rev], e.g ['branch', 'master']
1820 # always should return [rev_type, rev], e.g ['branch', 'master']
1815 if self._landing_revision:
1821 if self._landing_revision:
1816 _rev_info = self._landing_revision.split(':')
1822 _rev_info = self._landing_revision.split(':')
1817 if len(_rev_info) < 2:
1823 if len(_rev_info) < 2:
1818 _rev_info.insert(0, 'rev')
1824 _rev_info.insert(0, 'rev')
1819 return [_rev_info[0], _rev_info[1]]
1825 return [_rev_info[0], _rev_info[1]]
1820 return [None, None]
1826 return [None, None]
1821
1827
1822 @property
1828 @property
1823 def landing_ref_type(self):
1829 def landing_ref_type(self):
1824 return self.landing_rev[0]
1830 return self.landing_rev[0]
1825
1831
1826 @property
1832 @property
1827 def landing_ref_name(self):
1833 def landing_ref_name(self):
1828 return self.landing_rev[1]
1834 return self.landing_rev[1]
1829
1835
1830 @landing_rev.setter
1836 @landing_rev.setter
1831 def landing_rev(self, val):
1837 def landing_rev(self, val):
1832 if ':' not in val:
1838 if ':' not in val:
1833 raise ValueError('value must be delimited with `:` and consist '
1839 raise ValueError('value must be delimited with `:` and consist '
1834 'of <rev_type>:<rev>, got %s instead' % val)
1840 'of <rev_type>:<rev>, got %s instead' % val)
1835 self._landing_revision = val
1841 self._landing_revision = val
1836
1842
1837 @hybrid_property
1843 @hybrid_property
1838 def locked(self):
1844 def locked(self):
1839 if self._locked:
1845 if self._locked:
1840 user_id, timelocked, reason = self._locked.split(':')
1846 user_id, timelocked, reason = self._locked.split(':')
1841 lock_values = int(user_id), timelocked, reason
1847 lock_values = int(user_id), timelocked, reason
1842 else:
1848 else:
1843 lock_values = [None, None, None]
1849 lock_values = [None, None, None]
1844 return lock_values
1850 return lock_values
1845
1851
1846 @locked.setter
1852 @locked.setter
1847 def locked(self, val):
1853 def locked(self, val):
1848 if val and isinstance(val, (list, tuple)):
1854 if val and isinstance(val, (list, tuple)):
1849 self._locked = ':'.join(map(str, val))
1855 self._locked = ':'.join(map(str, val))
1850 else:
1856 else:
1851 self._locked = None
1857 self._locked = None
1852
1858
1853 @classmethod
1859 @classmethod
1854 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1860 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1855 from rhodecode.lib.vcs.backends.base import EmptyCommit
1861 from rhodecode.lib.vcs.backends.base import EmptyCommit
1856 dummy = EmptyCommit().__json__()
1862 dummy = EmptyCommit().__json__()
1857 if not changeset_cache_raw:
1863 if not changeset_cache_raw:
1858 dummy['source_repo_id'] = repo_id
1864 dummy['source_repo_id'] = repo_id
1859 return json.loads(json.dumps(dummy))
1865 return json.loads(json.dumps(dummy))
1860
1866
1861 try:
1867 try:
1862 return json.loads(changeset_cache_raw)
1868 return json.loads(changeset_cache_raw)
1863 except TypeError:
1869 except TypeError:
1864 return dummy
1870 return dummy
1865 except Exception:
1871 except Exception:
1866 log.error(traceback.format_exc())
1872 log.error(traceback.format_exc())
1867 return dummy
1873 return dummy
1868
1874
1869 @hybrid_property
1875 @hybrid_property
1870 def changeset_cache(self):
1876 def changeset_cache(self):
1871 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1877 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1872
1878
1873 @changeset_cache.setter
1879 @changeset_cache.setter
1874 def changeset_cache(self, val):
1880 def changeset_cache(self, val):
1875 try:
1881 try:
1876 self._changeset_cache = json.dumps(val)
1882 self._changeset_cache = json.dumps(val)
1877 except Exception:
1883 except Exception:
1878 log.error(traceback.format_exc())
1884 log.error(traceback.format_exc())
1879
1885
1880 @hybrid_property
1886 @hybrid_property
1881 def repo_name(self):
1887 def repo_name(self):
1882 return self._repo_name
1888 return self._repo_name
1883
1889
1884 @repo_name.setter
1890 @repo_name.setter
1885 def repo_name(self, value):
1891 def repo_name(self, value):
1886 self._repo_name = value
1892 self._repo_name = value
1887 self.repo_name_hash = sha1(safe_bytes(value))
1893 self.repo_name_hash = sha1(safe_bytes(value))
1888
1894
1889 @classmethod
1895 @classmethod
1890 def normalize_repo_name(cls, repo_name):
1896 def normalize_repo_name(cls, repo_name):
1891 """
1897 """
1892 Normalizes os specific repo_name to the format internally stored inside
1898 Normalizes os specific repo_name to the format internally stored inside
1893 database using URL_SEP
1899 database using URL_SEP
1894
1900
1895 :param cls:
1901 :param cls:
1896 :param repo_name:
1902 :param repo_name:
1897 """
1903 """
1898 return cls.NAME_SEP.join(repo_name.split(os.sep))
1904 return cls.NAME_SEP.join(repo_name.split(os.sep))
1899
1905
1900 @classmethod
1906 @classmethod
1901 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1907 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1902 session = Session()
1908 session = Session()
1903 q = session.query(cls).filter(cls.repo_name == repo_name)
1909 q = session.query(cls).filter(cls.repo_name == repo_name)
1904
1910
1905 if cache:
1911 if cache:
1906 if identity_cache:
1912 if identity_cache:
1907 val = cls.identity_cache(session, 'repo_name', repo_name)
1913 val = cls.identity_cache(session, 'repo_name', repo_name)
1908 if val:
1914 if val:
1909 return val
1915 return val
1910 else:
1916 else:
1911 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1917 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1912 q = q.options(
1918 q = q.options(
1913 FromCache("sql_cache_short", cache_key))
1919 FromCache("sql_cache_short", cache_key))
1914
1920
1915 return q.scalar()
1921 return q.scalar()
1916
1922
1917 @classmethod
1923 @classmethod
1918 def get_by_id_or_repo_name(cls, repoid):
1924 def get_by_id_or_repo_name(cls, repoid):
1919 if isinstance(repoid, int):
1925 if isinstance(repoid, int):
1920 try:
1926 try:
1921 repo = cls.get(repoid)
1927 repo = cls.get(repoid)
1922 except ValueError:
1928 except ValueError:
1923 repo = None
1929 repo = None
1924 else:
1930 else:
1925 repo = cls.get_by_repo_name(repoid)
1931 repo = cls.get_by_repo_name(repoid)
1926 return repo
1932 return repo
1927
1933
1928 @classmethod
1934 @classmethod
1929 def get_by_full_path(cls, repo_full_path):
1935 def get_by_full_path(cls, repo_full_path):
1930 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1936 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1931 repo_name = cls.normalize_repo_name(repo_name)
1937 repo_name = cls.normalize_repo_name(repo_name)
1932 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1938 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1933
1939
1934 @classmethod
1940 @classmethod
1935 def get_repo_forks(cls, repo_id):
1941 def get_repo_forks(cls, repo_id):
1936 return cls.query().filter(Repository.fork_id == repo_id)
1942 return cls.query().filter(Repository.fork_id == repo_id)
1937
1943
1938 @classmethod
1944 @classmethod
1939 def base_path(cls):
1945 def base_path(cls):
1940 """
1946 """
1941 Returns base path when all repos are stored
1947 Returns base path when all repos are stored
1942
1948
1943 :param cls:
1949 :param cls:
1944 """
1950 """
1945 from rhodecode.lib.utils import get_rhodecode_repo_store_path
1951 from rhodecode.lib.utils import get_rhodecode_repo_store_path
1946 return get_rhodecode_repo_store_path()
1952 return get_rhodecode_repo_store_path()
1947
1953
1948 @classmethod
1954 @classmethod
1949 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1955 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1950 case_insensitive=True, archived=False):
1956 case_insensitive=True, archived=False):
1951 q = Repository.query()
1957 q = Repository.query()
1952
1958
1953 if not archived:
1959 if not archived:
1954 q = q.filter(Repository.archived.isnot(true()))
1960 q = q.filter(Repository.archived.isnot(true()))
1955
1961
1956 if not isinstance(user_id, Optional):
1962 if not isinstance(user_id, Optional):
1957 q = q.filter(Repository.user_id == user_id)
1963 q = q.filter(Repository.user_id == user_id)
1958
1964
1959 if not isinstance(group_id, Optional):
1965 if not isinstance(group_id, Optional):
1960 q = q.filter(Repository.group_id == group_id)
1966 q = q.filter(Repository.group_id == group_id)
1961
1967
1962 if case_insensitive:
1968 if case_insensitive:
1963 q = q.order_by(func.lower(Repository.repo_name))
1969 q = q.order_by(func.lower(Repository.repo_name))
1964 else:
1970 else:
1965 q = q.order_by(Repository.repo_name)
1971 q = q.order_by(Repository.repo_name)
1966
1972
1967 return q.all()
1973 return q.all()
1968
1974
1969 @property
1975 @property
1970 def repo_uid(self):
1976 def repo_uid(self):
1971 return '_{}'.format(self.repo_id)
1977 return '_{}'.format(self.repo_id)
1972
1978
1973 @property
1979 @property
1974 def forks(self):
1980 def forks(self):
1975 """
1981 """
1976 Return forks of this repo
1982 Return forks of this repo
1977 """
1983 """
1978 return Repository.get_repo_forks(self.repo_id)
1984 return Repository.get_repo_forks(self.repo_id)
1979
1985
1980 @property
1986 @property
1981 def parent(self):
1987 def parent(self):
1982 """
1988 """
1983 Returns fork parent
1989 Returns fork parent
1984 """
1990 """
1985 return self.fork
1991 return self.fork
1986
1992
1987 @property
1993 @property
1988 def just_name(self):
1994 def just_name(self):
1989 return self.repo_name.split(self.NAME_SEP)[-1]
1995 return self.repo_name.split(self.NAME_SEP)[-1]
1990
1996
1991 @property
1997 @property
1992 def groups_with_parents(self):
1998 def groups_with_parents(self):
1993 groups = []
1999 groups = []
1994 if self.group is None:
2000 if self.group is None:
1995 return groups
2001 return groups
1996
2002
1997 cur_gr = self.group
2003 cur_gr = self.group
1998 groups.insert(0, cur_gr)
2004 groups.insert(0, cur_gr)
1999 while 1:
2005 while 1:
2000 gr = getattr(cur_gr, 'parent_group', None)
2006 gr = getattr(cur_gr, 'parent_group', None)
2001 cur_gr = cur_gr.parent_group
2007 cur_gr = cur_gr.parent_group
2002 if gr is None:
2008 if gr is None:
2003 break
2009 break
2004 groups.insert(0, gr)
2010 groups.insert(0, gr)
2005
2011
2006 return groups
2012 return groups
2007
2013
2008 @property
2014 @property
2009 def groups_and_repo(self):
2015 def groups_and_repo(self):
2010 return self.groups_with_parents, self
2016 return self.groups_with_parents, self
2011
2017
2012 @property
2018 @property
2013 def repo_path(self):
2019 def repo_path(self):
2014 """
2020 """
2015 Returns base full path for that repository means where it actually
2021 Returns base full path for that repository means where it actually
2016 exists on a filesystem
2022 exists on a filesystem
2017 """
2023 """
2018 return self.base_path()
2024 return self.base_path()
2019
2025
2020 @property
2026 @property
2021 def repo_full_path(self):
2027 def repo_full_path(self):
2022 p = [self.repo_path]
2028 p = [self.repo_path]
2023 # we need to split the name by / since this is how we store the
2029 # we need to split the name by / since this is how we store the
2024 # names in the database, but that eventually needs to be converted
2030 # names in the database, but that eventually needs to be converted
2025 # into a valid system path
2031 # into a valid system path
2026 p += self.repo_name.split(self.NAME_SEP)
2032 p += self.repo_name.split(self.NAME_SEP)
2027 return os.path.join(*map(safe_str, p))
2033 return os.path.join(*map(safe_str, p))
2028
2034
2029 @property
2035 @property
2030 def cache_keys(self):
2036 def cache_keys(self):
2031 """
2037 """
2032 Returns associated cache keys for that repo
2038 Returns associated cache keys for that repo
2033 """
2039 """
2034 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2040 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2035 return CacheKey.query()\
2041 return CacheKey.query()\
2036 .filter(CacheKey.cache_key == repo_namespace_key)\
2042 .filter(CacheKey.cache_key == repo_namespace_key)\
2037 .order_by(CacheKey.cache_key)\
2043 .order_by(CacheKey.cache_key)\
2038 .all()
2044 .all()
2039
2045
2040 @property
2046 @property
2041 def cached_diffs_relative_dir(self):
2047 def cached_diffs_relative_dir(self):
2042 """
2048 """
2043 Return a relative to the repository store path of cached diffs
2049 Return a relative to the repository store path of cached diffs
2044 used for safe display for users, who shouldn't know the absolute store
2050 used for safe display for users, who shouldn't know the absolute store
2045 path
2051 path
2046 """
2052 """
2047 return os.path.join(
2053 return os.path.join(
2048 os.path.dirname(self.repo_name),
2054 os.path.dirname(self.repo_name),
2049 self.cached_diffs_dir.split(os.path.sep)[-1])
2055 self.cached_diffs_dir.split(os.path.sep)[-1])
2050
2056
2051 @property
2057 @property
2052 def cached_diffs_dir(self):
2058 def cached_diffs_dir(self):
2053 path = self.repo_full_path
2059 path = self.repo_full_path
2054 return os.path.join(
2060 return os.path.join(
2055 os.path.dirname(path),
2061 os.path.dirname(path),
2056 f'.__shadow_diff_cache_repo_{self.repo_id}')
2062 f'.__shadow_diff_cache_repo_{self.repo_id}')
2057
2063
2058 def cached_diffs(self):
2064 def cached_diffs(self):
2059 diff_cache_dir = self.cached_diffs_dir
2065 diff_cache_dir = self.cached_diffs_dir
2060 if os.path.isdir(diff_cache_dir):
2066 if os.path.isdir(diff_cache_dir):
2061 return os.listdir(diff_cache_dir)
2067 return os.listdir(diff_cache_dir)
2062 return []
2068 return []
2063
2069
2064 def shadow_repos(self):
2070 def shadow_repos(self):
2065 shadow_repos_pattern = f'.__shadow_repo_{self.repo_id}'
2071 shadow_repos_pattern = f'.__shadow_repo_{self.repo_id}'
2066 return [
2072 return [
2067 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2073 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2068 if x.startswith(shadow_repos_pattern)
2074 if x.startswith(shadow_repos_pattern)
2069 ]
2075 ]
2070
2076
2071 def get_new_name(self, repo_name):
2077 def get_new_name(self, repo_name):
2072 """
2078 """
2073 returns new full repository name based on assigned group and new new
2079 returns new full repository name based on assigned group and new new
2074
2080
2075 :param repo_name:
2081 :param repo_name:
2076 """
2082 """
2077 path_prefix = self.group.full_path_splitted if self.group else []
2083 path_prefix = self.group.full_path_splitted if self.group else []
2078 return self.NAME_SEP.join(path_prefix + [repo_name])
2084 return self.NAME_SEP.join(path_prefix + [repo_name])
2079
2085
2080 @property
2086 @property
2081 def _config(self):
2087 def _config(self):
2082 """
2088 """
2083 Returns db based config object.
2089 Returns db based config object.
2084 """
2090 """
2085 from rhodecode.lib.utils import make_db_config
2091 from rhodecode.lib.utils import make_db_config
2086 return make_db_config(clear_session=False, repo=self)
2092 return make_db_config(clear_session=False, repo=self)
2087
2093
2088 def permissions(self, with_admins=True, with_owner=True,
2094 def permissions(self, with_admins=True, with_owner=True,
2089 expand_from_user_groups=False):
2095 expand_from_user_groups=False):
2090 """
2096 """
2091 Permissions for repositories
2097 Permissions for repositories
2092 """
2098 """
2093 _admin_perm = 'repository.admin'
2099 _admin_perm = 'repository.admin'
2094
2100
2095 owner_row = []
2101 owner_row = []
2096 if with_owner:
2102 if with_owner:
2097 usr = AttributeDict(self.user.get_dict())
2103 usr = AttributeDict(self.user.get_dict())
2098 usr.owner_row = True
2104 usr.owner_row = True
2099 usr.permission = _admin_perm
2105 usr.permission = _admin_perm
2100 usr.permission_id = None
2106 usr.permission_id = None
2101 owner_row.append(usr)
2107 owner_row.append(usr)
2102
2108
2103 super_admin_ids = []
2109 super_admin_ids = []
2104 super_admin_rows = []
2110 super_admin_rows = []
2105 if with_admins:
2111 if with_admins:
2106 for usr in User.get_all_super_admins():
2112 for usr in User.get_all_super_admins():
2107 super_admin_ids.append(usr.user_id)
2113 super_admin_ids.append(usr.user_id)
2108 # if this admin is also owner, don't double the record
2114 # if this admin is also owner, don't double the record
2109 if usr.user_id == owner_row[0].user_id:
2115 if usr.user_id == owner_row[0].user_id:
2110 owner_row[0].admin_row = True
2116 owner_row[0].admin_row = True
2111 else:
2117 else:
2112 usr = AttributeDict(usr.get_dict())
2118 usr = AttributeDict(usr.get_dict())
2113 usr.admin_row = True
2119 usr.admin_row = True
2114 usr.permission = _admin_perm
2120 usr.permission = _admin_perm
2115 usr.permission_id = None
2121 usr.permission_id = None
2116 super_admin_rows.append(usr)
2122 super_admin_rows.append(usr)
2117
2123
2118 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2124 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2119 q = q.options(joinedload(UserRepoToPerm.repository),
2125 q = q.options(joinedload(UserRepoToPerm.repository),
2120 joinedload(UserRepoToPerm.user),
2126 joinedload(UserRepoToPerm.user),
2121 joinedload(UserRepoToPerm.permission),)
2127 joinedload(UserRepoToPerm.permission),)
2122
2128
2123 # get owners and admins and permissions. We do a trick of re-writing
2129 # get owners and admins and permissions. We do a trick of re-writing
2124 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2130 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2125 # has a global reference and changing one object propagates to all
2131 # has a global reference and changing one object propagates to all
2126 # others. This means if admin is also an owner admin_row that change
2132 # others. This means if admin is also an owner admin_row that change
2127 # would propagate to both objects
2133 # would propagate to both objects
2128 perm_rows = []
2134 perm_rows = []
2129 for _usr in q.all():
2135 for _usr in q.all():
2130 usr = AttributeDict(_usr.user.get_dict())
2136 usr = AttributeDict(_usr.user.get_dict())
2131 # if this user is also owner/admin, mark as duplicate record
2137 # if this user is also owner/admin, mark as duplicate record
2132 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2138 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2133 usr.duplicate_perm = True
2139 usr.duplicate_perm = True
2134 # also check if this permission is maybe used by branch_permissions
2140 # also check if this permission is maybe used by branch_permissions
2135 if _usr.branch_perm_entry:
2141 if _usr.branch_perm_entry:
2136 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2142 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2137
2143
2138 usr.permission = _usr.permission.permission_name
2144 usr.permission = _usr.permission.permission_name
2139 usr.permission_id = _usr.repo_to_perm_id
2145 usr.permission_id = _usr.repo_to_perm_id
2140 perm_rows.append(usr)
2146 perm_rows.append(usr)
2141
2147
2142 # filter the perm rows by 'default' first and then sort them by
2148 # filter the perm rows by 'default' first and then sort them by
2143 # admin,write,read,none permissions sorted again alphabetically in
2149 # admin,write,read,none permissions sorted again alphabetically in
2144 # each group
2150 # each group
2145 perm_rows = sorted(perm_rows, key=display_user_sort)
2151 perm_rows = sorted(perm_rows, key=display_user_sort)
2146
2152
2147 user_groups_rows = []
2153 user_groups_rows = []
2148 if expand_from_user_groups:
2154 if expand_from_user_groups:
2149 for ug in self.permission_user_groups(with_members=True):
2155 for ug in self.permission_user_groups(with_members=True):
2150 for user_data in ug.members:
2156 for user_data in ug.members:
2151 user_groups_rows.append(user_data)
2157 user_groups_rows.append(user_data)
2152
2158
2153 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2159 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2154
2160
2155 def permission_user_groups(self, with_members=True):
2161 def permission_user_groups(self, with_members=True):
2156 q = UserGroupRepoToPerm.query()\
2162 q = UserGroupRepoToPerm.query()\
2157 .filter(UserGroupRepoToPerm.repository == self)
2163 .filter(UserGroupRepoToPerm.repository == self)
2158 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2164 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2159 joinedload(UserGroupRepoToPerm.users_group),
2165 joinedload(UserGroupRepoToPerm.users_group),
2160 joinedload(UserGroupRepoToPerm.permission),)
2166 joinedload(UserGroupRepoToPerm.permission),)
2161
2167
2162 perm_rows = []
2168 perm_rows = []
2163 for _user_group in q.all():
2169 for _user_group in q.all():
2164 entry = AttributeDict(_user_group.users_group.get_dict())
2170 entry = AttributeDict(_user_group.users_group.get_dict())
2165 entry.permission = _user_group.permission.permission_name
2171 entry.permission = _user_group.permission.permission_name
2166 if with_members:
2172 if with_members:
2167 entry.members = [x.user.get_dict()
2173 entry.members = [x.user.get_dict()
2168 for x in _user_group.users_group.members]
2174 for x in _user_group.users_group.members]
2169 perm_rows.append(entry)
2175 perm_rows.append(entry)
2170
2176
2171 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2177 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2172 return perm_rows
2178 return perm_rows
2173
2179
2174 def get_api_data(self, include_secrets=False):
2180 def get_api_data(self, include_secrets=False):
2175 """
2181 """
2176 Common function for generating repo api data
2182 Common function for generating repo api data
2177
2183
2178 :param include_secrets: See :meth:`User.get_api_data`.
2184 :param include_secrets: See :meth:`User.get_api_data`.
2179
2185
2180 """
2186 """
2181 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2187 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2182 # move this methods on models level.
2188 # move this methods on models level.
2183 from rhodecode.model.settings import SettingsModel
2189 from rhodecode.model.settings import SettingsModel
2184 from rhodecode.model.repo import RepoModel
2190 from rhodecode.model.repo import RepoModel
2185
2191
2186 repo = self
2192 repo = self
2187 _user_id, _time, _reason = self.locked
2193 _user_id, _time, _reason = self.locked
2188
2194
2189 data = {
2195 data = {
2190 'repo_id': repo.repo_id,
2196 'repo_id': repo.repo_id,
2191 'repo_name': repo.repo_name,
2197 'repo_name': repo.repo_name,
2192 'repo_type': repo.repo_type,
2198 'repo_type': repo.repo_type,
2193 'clone_uri': repo.clone_uri or '',
2199 'clone_uri': repo.clone_uri or '',
2194 'push_uri': repo.push_uri or '',
2200 'push_uri': repo.push_uri or '',
2195 'url': RepoModel().get_url(self),
2201 'url': RepoModel().get_url(self),
2196 'private': repo.private,
2202 'private': repo.private,
2197 'created_on': repo.created_on,
2203 'created_on': repo.created_on,
2198 'description': repo.description_safe,
2204 'description': repo.description_safe,
2199 'landing_rev': repo.landing_rev,
2205 'landing_rev': repo.landing_rev,
2200 'owner': repo.user.username,
2206 'owner': repo.user.username,
2201 'fork_of': repo.fork.repo_name if repo.fork else None,
2207 'fork_of': repo.fork.repo_name if repo.fork else None,
2202 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2208 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2203 'enable_statistics': repo.enable_statistics,
2209 'enable_statistics': repo.enable_statistics,
2204 'enable_locking': repo.enable_locking,
2210 'enable_locking': repo.enable_locking,
2205 'enable_downloads': repo.enable_downloads,
2211 'enable_downloads': repo.enable_downloads,
2206 'last_changeset': repo.changeset_cache,
2212 'last_changeset': repo.changeset_cache,
2207 'locked_by': User.get(_user_id).get_api_data(
2213 'locked_by': User.get(_user_id).get_api_data(
2208 include_secrets=include_secrets) if _user_id else None,
2214 include_secrets=include_secrets) if _user_id else None,
2209 'locked_date': time_to_datetime(_time) if _time else None,
2215 'locked_date': time_to_datetime(_time) if _time else None,
2210 'lock_reason': _reason if _reason else None,
2216 'lock_reason': _reason if _reason else None,
2211 }
2217 }
2212
2218
2213 # TODO: mikhail: should be per-repo settings here
2219 # TODO: mikhail: should be per-repo settings here
2214 rc_config = SettingsModel().get_all_settings()
2220 rc_config = SettingsModel().get_all_settings()
2215 repository_fields = str2bool(
2221 repository_fields = str2bool(
2216 rc_config.get('rhodecode_repository_fields'))
2222 rc_config.get('rhodecode_repository_fields'))
2217 if repository_fields:
2223 if repository_fields:
2218 for f in self.extra_fields:
2224 for f in self.extra_fields:
2219 data[f.field_key_prefixed] = f.field_value
2225 data[f.field_key_prefixed] = f.field_value
2220
2226
2221 return data
2227 return data
2222
2228
2223 @classmethod
2229 @classmethod
2224 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2230 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2225 if not lock_time:
2231 if not lock_time:
2226 lock_time = time.time()
2232 lock_time = time.time()
2227 if not lock_reason:
2233 if not lock_reason:
2228 lock_reason = cls.LOCK_AUTOMATIC
2234 lock_reason = cls.LOCK_AUTOMATIC
2229 repo.locked = [user_id, lock_time, lock_reason]
2235 repo.locked = [user_id, lock_time, lock_reason]
2230 Session().add(repo)
2236 Session().add(repo)
2231 Session().commit()
2237 Session().commit()
2232
2238
2233 @classmethod
2239 @classmethod
2234 def unlock(cls, repo):
2240 def unlock(cls, repo):
2235 repo.locked = None
2241 repo.locked = None
2236 Session().add(repo)
2242 Session().add(repo)
2237 Session().commit()
2243 Session().commit()
2238
2244
2239 @classmethod
2245 @classmethod
2240 def getlock(cls, repo):
2246 def getlock(cls, repo):
2241 return repo.locked
2247 return repo.locked
2242
2248
2243 def get_locking_state(self, action, user_id, only_when_enabled=True):
2249 def get_locking_state(self, action, user_id, only_when_enabled=True):
2244 """
2250 """
2245 Checks locking on this repository, if locking is enabled and lock is
2251 Checks locking on this repository, if locking is enabled and lock is
2246 present returns a tuple of make_lock, locked, locked_by.
2252 present returns a tuple of make_lock, locked, locked_by.
2247 make_lock can have 3 states None (do nothing) True, make lock
2253 make_lock can have 3 states None (do nothing) True, make lock
2248 False release lock, This value is later propagated to hooks, which
2254 False release lock, This value is later propagated to hooks, which
2249 do the locking. Think about this as signals passed to hooks what to do.
2255 do the locking. Think about this as signals passed to hooks what to do.
2250
2256
2251 """
2257 """
2252 # TODO: johbo: This is part of the business logic and should be moved
2258 # TODO: johbo: This is part of the business logic and should be moved
2253 # into the RepositoryModel.
2259 # into the RepositoryModel.
2254
2260
2255 if action not in ('push', 'pull'):
2261 if action not in ('push', 'pull'):
2256 raise ValueError("Invalid action value: %s" % repr(action))
2262 raise ValueError("Invalid action value: %s" % repr(action))
2257
2263
2258 # defines if locked error should be thrown to user
2264 # defines if locked error should be thrown to user
2259 currently_locked = False
2265 currently_locked = False
2260 # defines if new lock should be made, tri-state
2266 # defines if new lock should be made, tri-state
2261 make_lock = None
2267 make_lock = None
2262 repo = self
2268 repo = self
2263 user = User.get(user_id)
2269 user = User.get(user_id)
2264
2270
2265 lock_info = repo.locked
2271 lock_info = repo.locked
2266
2272
2267 if repo and (repo.enable_locking or not only_when_enabled):
2273 if repo and (repo.enable_locking or not only_when_enabled):
2268 if action == 'push':
2274 if action == 'push':
2269 # check if it's already locked !, if it is compare users
2275 # check if it's already locked !, if it is compare users
2270 locked_by_user_id = lock_info[0]
2276 locked_by_user_id = lock_info[0]
2271 if user.user_id == locked_by_user_id:
2277 if user.user_id == locked_by_user_id:
2272 log.debug(
2278 log.debug(
2273 'Got `push` action from user %s, now unlocking', user)
2279 'Got `push` action from user %s, now unlocking', user)
2274 # unlock if we have push from user who locked
2280 # unlock if we have push from user who locked
2275 make_lock = False
2281 make_lock = False
2276 else:
2282 else:
2277 # we're not the same user who locked, ban with
2283 # we're not the same user who locked, ban with
2278 # code defined in settings (default is 423 HTTP Locked) !
2284 # code defined in settings (default is 423 HTTP Locked) !
2279 log.debug('Repo %s is currently locked by %s', repo, user)
2285 log.debug('Repo %s is currently locked by %s', repo, user)
2280 currently_locked = True
2286 currently_locked = True
2281 elif action == 'pull':
2287 elif action == 'pull':
2282 # [0] user [1] date
2288 # [0] user [1] date
2283 if lock_info[0] and lock_info[1]:
2289 if lock_info[0] and lock_info[1]:
2284 log.debug('Repo %s is currently locked by %s', repo, user)
2290 log.debug('Repo %s is currently locked by %s', repo, user)
2285 currently_locked = True
2291 currently_locked = True
2286 else:
2292 else:
2287 log.debug('Setting lock on repo %s by %s', repo, user)
2293 log.debug('Setting lock on repo %s by %s', repo, user)
2288 make_lock = True
2294 make_lock = True
2289
2295
2290 else:
2296 else:
2291 log.debug('Repository %s do not have locking enabled', repo)
2297 log.debug('Repository %s do not have locking enabled', repo)
2292
2298
2293 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2299 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2294 make_lock, currently_locked, lock_info)
2300 make_lock, currently_locked, lock_info)
2295
2301
2296 from rhodecode.lib.auth import HasRepoPermissionAny
2302 from rhodecode.lib.auth import HasRepoPermissionAny
2297 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2303 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2298 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2304 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2299 # if we don't have at least write permission we cannot make a lock
2305 # if we don't have at least write permission we cannot make a lock
2300 log.debug('lock state reset back to FALSE due to lack '
2306 log.debug('lock state reset back to FALSE due to lack '
2301 'of at least read permission')
2307 'of at least read permission')
2302 make_lock = False
2308 make_lock = False
2303
2309
2304 return make_lock, currently_locked, lock_info
2310 return make_lock, currently_locked, lock_info
2305
2311
2306 @property
2312 @property
2307 def last_commit_cache_update_diff(self):
2313 def last_commit_cache_update_diff(self):
2308 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2314 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2309
2315
2310 @classmethod
2316 @classmethod
2311 def _load_commit_change(cls, last_commit_cache):
2317 def _load_commit_change(cls, last_commit_cache):
2312 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2318 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2313 empty_date = datetime.datetime.fromtimestamp(0)
2319 empty_date = datetime.datetime.fromtimestamp(0)
2314 date_latest = last_commit_cache.get('date', empty_date)
2320 date_latest = last_commit_cache.get('date', empty_date)
2315 try:
2321 try:
2316 return parse_datetime(date_latest)
2322 return parse_datetime(date_latest)
2317 except Exception:
2323 except Exception:
2318 return empty_date
2324 return empty_date
2319
2325
2320 @property
2326 @property
2321 def last_commit_change(self):
2327 def last_commit_change(self):
2322 return self._load_commit_change(self.changeset_cache)
2328 return self._load_commit_change(self.changeset_cache)
2323
2329
2324 @property
2330 @property
2325 def last_db_change(self):
2331 def last_db_change(self):
2326 return self.updated_on
2332 return self.updated_on
2327
2333
2328 @property
2334 @property
2329 def clone_uri_hidden(self):
2335 def clone_uri_hidden(self):
2330 clone_uri = self.clone_uri
2336 clone_uri = self.clone_uri
2331 if clone_uri:
2337 if clone_uri:
2332 import urlobject
2338 import urlobject
2333 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2339 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2334 if url_obj.password:
2340 if url_obj.password:
2335 clone_uri = url_obj.with_password('*****')
2341 clone_uri = url_obj.with_password('*****')
2336 return clone_uri
2342 return clone_uri
2337
2343
2338 @property
2344 @property
2339 def push_uri_hidden(self):
2345 def push_uri_hidden(self):
2340 push_uri = self.push_uri
2346 push_uri = self.push_uri
2341 if push_uri:
2347 if push_uri:
2342 import urlobject
2348 import urlobject
2343 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2349 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2344 if url_obj.password:
2350 if url_obj.password:
2345 push_uri = url_obj.with_password('*****')
2351 push_uri = url_obj.with_password('*****')
2346 return push_uri
2352 return push_uri
2347
2353
2348 def clone_url(self, **override):
2354 def clone_url(self, **override):
2349 from rhodecode.model.settings import SettingsModel
2355 from rhodecode.model.settings import SettingsModel
2350
2356
2351 uri_tmpl = None
2357 uri_tmpl = None
2352 if 'with_id' in override:
2358 if 'with_id' in override:
2353 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2359 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2354 del override['with_id']
2360 del override['with_id']
2355
2361
2356 if 'uri_tmpl' in override:
2362 if 'uri_tmpl' in override:
2357 uri_tmpl = override['uri_tmpl']
2363 uri_tmpl = override['uri_tmpl']
2358 del override['uri_tmpl']
2364 del override['uri_tmpl']
2359
2365
2360 ssh = False
2366 ssh = False
2361 if 'ssh' in override:
2367 if 'ssh' in override:
2362 ssh = True
2368 ssh = True
2363 del override['ssh']
2369 del override['ssh']
2364
2370
2365 # we didn't override our tmpl from **overrides
2371 # we didn't override our tmpl from **overrides
2366 request = get_current_request()
2372 request = get_current_request()
2367 if not uri_tmpl:
2373 if not uri_tmpl:
2368 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2374 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2369 rc_config = request.call_context.rc_config
2375 rc_config = request.call_context.rc_config
2370 else:
2376 else:
2371 rc_config = SettingsModel().get_all_settings(cache=True)
2377 rc_config = SettingsModel().get_all_settings(cache=True)
2372
2378
2373 if ssh:
2379 if ssh:
2374 uri_tmpl = rc_config.get(
2380 uri_tmpl = rc_config.get(
2375 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2381 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2376
2382
2377 else:
2383 else:
2378 uri_tmpl = rc_config.get(
2384 uri_tmpl = rc_config.get(
2379 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2385 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2380
2386
2381 return get_clone_url(request=request,
2387 return get_clone_url(request=request,
2382 uri_tmpl=uri_tmpl,
2388 uri_tmpl=uri_tmpl,
2383 repo_name=self.repo_name,
2389 repo_name=self.repo_name,
2384 repo_id=self.repo_id,
2390 repo_id=self.repo_id,
2385 repo_type=self.repo_type,
2391 repo_type=self.repo_type,
2386 **override)
2392 **override)
2387
2393
2388 def set_state(self, state):
2394 def set_state(self, state):
2389 self.repo_state = state
2395 self.repo_state = state
2390 Session().add(self)
2396 Session().add(self)
2391 #==========================================================================
2397 #==========================================================================
2392 # SCM PROPERTIES
2398 # SCM PROPERTIES
2393 #==========================================================================
2399 #==========================================================================
2394
2400
2395 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False, reference_obj=None):
2401 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False, reference_obj=None):
2396 return get_commit_safe(
2402 return get_commit_safe(
2397 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2403 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2398 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
2404 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
2399
2405
2400 def get_changeset(self, rev=None, pre_load=None):
2406 def get_changeset(self, rev=None, pre_load=None):
2401 warnings.warn("Use get_commit", DeprecationWarning)
2407 warnings.warn("Use get_commit", DeprecationWarning)
2402 commit_id = None
2408 commit_id = None
2403 commit_idx = None
2409 commit_idx = None
2404 if isinstance(rev, str):
2410 if isinstance(rev, str):
2405 commit_id = rev
2411 commit_id = rev
2406 else:
2412 else:
2407 commit_idx = rev
2413 commit_idx = rev
2408 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2414 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2409 pre_load=pre_load)
2415 pre_load=pre_load)
2410
2416
2411 def get_landing_commit(self):
2417 def get_landing_commit(self):
2412 """
2418 """
2413 Returns landing commit, or if that doesn't exist returns the tip
2419 Returns landing commit, or if that doesn't exist returns the tip
2414 """
2420 """
2415 _rev_type, _rev = self.landing_rev
2421 _rev_type, _rev = self.landing_rev
2416 commit = self.get_commit(_rev)
2422 commit = self.get_commit(_rev)
2417 if isinstance(commit, EmptyCommit):
2423 if isinstance(commit, EmptyCommit):
2418 return self.get_commit()
2424 return self.get_commit()
2419 return commit
2425 return commit
2420
2426
2421 def flush_commit_cache(self):
2427 def flush_commit_cache(self):
2422 self.update_commit_cache(cs_cache={'raw_id':'0'})
2428 self.update_commit_cache(cs_cache={'raw_id':'0'})
2423 self.update_commit_cache()
2429 self.update_commit_cache()
2424
2430
2425 def update_commit_cache(self, cs_cache=None, config=None):
2431 def update_commit_cache(self, cs_cache=None, config=None):
2426 """
2432 """
2427 Update cache of last commit for repository
2433 Update cache of last commit for repository
2428 cache_keys should be::
2434 cache_keys should be::
2429
2435
2430 source_repo_id
2436 source_repo_id
2431 short_id
2437 short_id
2432 raw_id
2438 raw_id
2433 revision
2439 revision
2434 parents
2440 parents
2435 message
2441 message
2436 date
2442 date
2437 author
2443 author
2438 updated_on
2444 updated_on
2439
2445
2440 """
2446 """
2441 from rhodecode.lib.vcs.backends.base import BaseCommit
2447 from rhodecode.lib.vcs.backends.base import BaseCommit
2442 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2448 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2443 empty_date = datetime.datetime.fromtimestamp(0)
2449 empty_date = datetime.datetime.fromtimestamp(0)
2444 repo_commit_count = 0
2450 repo_commit_count = 0
2445
2451
2446 if cs_cache is None:
2452 if cs_cache is None:
2447 # use no-cache version here
2453 # use no-cache version here
2448 try:
2454 try:
2449 scm_repo = self.scm_instance(cache=False, config=config)
2455 scm_repo = self.scm_instance(cache=False, config=config)
2450 except VCSError:
2456 except VCSError:
2451 scm_repo = None
2457 scm_repo = None
2452 empty = scm_repo is None or scm_repo.is_empty()
2458 empty = scm_repo is None or scm_repo.is_empty()
2453
2459
2454 if not empty:
2460 if not empty:
2455 cs_cache = scm_repo.get_commit(
2461 cs_cache = scm_repo.get_commit(
2456 pre_load=["author", "date", "message", "parents", "branch"])
2462 pre_load=["author", "date", "message", "parents", "branch"])
2457 repo_commit_count = scm_repo.count()
2463 repo_commit_count = scm_repo.count()
2458 else:
2464 else:
2459 cs_cache = EmptyCommit()
2465 cs_cache = EmptyCommit()
2460
2466
2461 if isinstance(cs_cache, BaseCommit):
2467 if isinstance(cs_cache, BaseCommit):
2462 cs_cache = cs_cache.__json__()
2468 cs_cache = cs_cache.__json__()
2463
2469
2464 def is_outdated(new_cs_cache):
2470 def is_outdated(new_cs_cache):
2465 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2471 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2466 new_cs_cache['revision'] != self.changeset_cache['revision']):
2472 new_cs_cache['revision'] != self.changeset_cache['revision']):
2467 return True
2473 return True
2468 return False
2474 return False
2469
2475
2470 # check if we have maybe already latest cached revision
2476 # check if we have maybe already latest cached revision
2471 if is_outdated(cs_cache) or not self.changeset_cache:
2477 if is_outdated(cs_cache) or not self.changeset_cache:
2472 _current_datetime = datetime.datetime.utcnow()
2478 _current_datetime = datetime.datetime.utcnow()
2473 last_change = cs_cache.get('date') or _current_datetime
2479 last_change = cs_cache.get('date') or _current_datetime
2474 # we check if last update is newer than the new value
2480 # we check if last update is newer than the new value
2475 # if yes, we use the current timestamp instead. Imagine you get
2481 # if yes, we use the current timestamp instead. Imagine you get
2476 # old commit pushed 1y ago, we'd set last update 1y to ago.
2482 # old commit pushed 1y ago, we'd set last update 1y to ago.
2477 last_change_timestamp = datetime_to_time(last_change)
2483 last_change_timestamp = datetime_to_time(last_change)
2478 current_timestamp = datetime_to_time(last_change)
2484 current_timestamp = datetime_to_time(last_change)
2479 if last_change_timestamp > current_timestamp and not empty:
2485 if last_change_timestamp > current_timestamp and not empty:
2480 cs_cache['date'] = _current_datetime
2486 cs_cache['date'] = _current_datetime
2481
2487
2482 # also store size of repo
2488 # also store size of repo
2483 cs_cache['repo_commit_count'] = repo_commit_count
2489 cs_cache['repo_commit_count'] = repo_commit_count
2484
2490
2485 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2491 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2486 cs_cache['updated_on'] = time.time()
2492 cs_cache['updated_on'] = time.time()
2487 self.changeset_cache = cs_cache
2493 self.changeset_cache = cs_cache
2488 self.updated_on = last_change
2494 self.updated_on = last_change
2489 Session().add(self)
2495 Session().add(self)
2490 Session().commit()
2496 Session().commit()
2491
2497
2492 else:
2498 else:
2493 if empty:
2499 if empty:
2494 cs_cache = EmptyCommit().__json__()
2500 cs_cache = EmptyCommit().__json__()
2495 else:
2501 else:
2496 cs_cache = self.changeset_cache
2502 cs_cache = self.changeset_cache
2497
2503
2498 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2504 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2499
2505
2500 cs_cache['updated_on'] = time.time()
2506 cs_cache['updated_on'] = time.time()
2501 self.changeset_cache = cs_cache
2507 self.changeset_cache = cs_cache
2502 self.updated_on = _date_latest
2508 self.updated_on = _date_latest
2503 Session().add(self)
2509 Session().add(self)
2504 Session().commit()
2510 Session().commit()
2505
2511
2506 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2512 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2507 self.repo_name, cs_cache, _date_latest)
2513 self.repo_name, cs_cache, _date_latest)
2508
2514
2509 @property
2515 @property
2510 def tip(self):
2516 def tip(self):
2511 return self.get_commit('tip')
2517 return self.get_commit('tip')
2512
2518
2513 @property
2519 @property
2514 def author(self):
2520 def author(self):
2515 return self.tip.author
2521 return self.tip.author
2516
2522
2517 @property
2523 @property
2518 def last_change(self):
2524 def last_change(self):
2519 return self.scm_instance().last_change
2525 return self.scm_instance().last_change
2520
2526
2521 def get_comments(self, revisions=None):
2527 def get_comments(self, revisions=None):
2522 """
2528 """
2523 Returns comments for this repository grouped by revisions
2529 Returns comments for this repository grouped by revisions
2524
2530
2525 :param revisions: filter query by revisions only
2531 :param revisions: filter query by revisions only
2526 """
2532 """
2527 cmts = ChangesetComment.query()\
2533 cmts = ChangesetComment.query()\
2528 .filter(ChangesetComment.repo == self)
2534 .filter(ChangesetComment.repo == self)
2529 if revisions:
2535 if revisions:
2530 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2536 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2531 grouped = collections.defaultdict(list)
2537 grouped = collections.defaultdict(list)
2532 for cmt in cmts.all():
2538 for cmt in cmts.all():
2533 grouped[cmt.revision].append(cmt)
2539 grouped[cmt.revision].append(cmt)
2534 return grouped
2540 return grouped
2535
2541
2536 def statuses(self, revisions=None):
2542 def statuses(self, revisions=None):
2537 """
2543 """
2538 Returns statuses for this repository
2544 Returns statuses for this repository
2539
2545
2540 :param revisions: list of revisions to get statuses for
2546 :param revisions: list of revisions to get statuses for
2541 """
2547 """
2542 statuses = ChangesetStatus.query()\
2548 statuses = ChangesetStatus.query()\
2543 .filter(ChangesetStatus.repo == self)\
2549 .filter(ChangesetStatus.repo == self)\
2544 .filter(ChangesetStatus.version == 0)
2550 .filter(ChangesetStatus.version == 0)
2545
2551
2546 if revisions:
2552 if revisions:
2547 # Try doing the filtering in chunks to avoid hitting limits
2553 # Try doing the filtering in chunks to avoid hitting limits
2548 size = 500
2554 size = 500
2549 status_results = []
2555 status_results = []
2550 for chunk in range(0, len(revisions), size):
2556 for chunk in range(0, len(revisions), size):
2551 status_results += statuses.filter(
2557 status_results += statuses.filter(
2552 ChangesetStatus.revision.in_(
2558 ChangesetStatus.revision.in_(
2553 revisions[chunk: chunk+size])
2559 revisions[chunk: chunk+size])
2554 ).all()
2560 ).all()
2555 else:
2561 else:
2556 status_results = statuses.all()
2562 status_results = statuses.all()
2557
2563
2558 grouped = {}
2564 grouped = {}
2559
2565
2560 # maybe we have open new pullrequest without a status?
2566 # maybe we have open new pullrequest without a status?
2561 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2567 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2562 status_lbl = ChangesetStatus.get_status_lbl(stat)
2568 status_lbl = ChangesetStatus.get_status_lbl(stat)
2563 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2569 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2564 for rev in pr.revisions:
2570 for rev in pr.revisions:
2565 pr_id = pr.pull_request_id
2571 pr_id = pr.pull_request_id
2566 pr_repo = pr.target_repo.repo_name
2572 pr_repo = pr.target_repo.repo_name
2567 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2573 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2568
2574
2569 for stat in status_results:
2575 for stat in status_results:
2570 pr_id = pr_repo = None
2576 pr_id = pr_repo = None
2571 if stat.pull_request:
2577 if stat.pull_request:
2572 pr_id = stat.pull_request.pull_request_id
2578 pr_id = stat.pull_request.pull_request_id
2573 pr_repo = stat.pull_request.target_repo.repo_name
2579 pr_repo = stat.pull_request.target_repo.repo_name
2574 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2580 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2575 pr_id, pr_repo]
2581 pr_id, pr_repo]
2576 return grouped
2582 return grouped
2577
2583
2578 # ==========================================================================
2584 # ==========================================================================
2579 # SCM CACHE INSTANCE
2585 # SCM CACHE INSTANCE
2580 # ==========================================================================
2586 # ==========================================================================
2581
2587
2582 def scm_instance(self, **kwargs):
2588 def scm_instance(self, **kwargs):
2583 import rhodecode
2589 import rhodecode
2584
2590
2585 # Passing a config will not hit the cache currently only used
2591 # Passing a config will not hit the cache currently only used
2586 # for repo2dbmapper
2592 # for repo2dbmapper
2587 config = kwargs.pop('config', None)
2593 config = kwargs.pop('config', None)
2588 cache = kwargs.pop('cache', None)
2594 cache = kwargs.pop('cache', None)
2589 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2595 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2590 if vcs_full_cache is not None:
2596 if vcs_full_cache is not None:
2591 # allows override global config
2597 # allows override global config
2592 full_cache = vcs_full_cache
2598 full_cache = vcs_full_cache
2593 else:
2599 else:
2594 full_cache = rhodecode.ConfigGet().get_bool('vcs_full_cache')
2600 full_cache = rhodecode.ConfigGet().get_bool('vcs_full_cache')
2595 # if cache is NOT defined use default global, else we have a full
2601 # if cache is NOT defined use default global, else we have a full
2596 # control over cache behaviour
2602 # control over cache behaviour
2597 if cache is None and full_cache and not config:
2603 if cache is None and full_cache and not config:
2598 log.debug('Initializing pure cached instance for %s', self.repo_path)
2604 log.debug('Initializing pure cached instance for %s', self.repo_path)
2599 return self._get_instance_cached()
2605 return self._get_instance_cached()
2600
2606
2601 # cache here is sent to the "vcs server"
2607 # cache here is sent to the "vcs server"
2602 return self._get_instance(cache=bool(cache), config=config)
2608 return self._get_instance(cache=bool(cache), config=config)
2603
2609
2604 def _get_instance_cached(self):
2610 def _get_instance_cached(self):
2605 from rhodecode.lib import rc_cache
2611 from rhodecode.lib import rc_cache
2606
2612
2607 cache_namespace_uid = f'repo_instance.{self.repo_id}'
2613 cache_namespace_uid = f'repo_instance.{self.repo_id}'
2608 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2614 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2609
2615
2610 # we must use thread scoped cache here,
2616 # we must use thread scoped cache here,
2611 # because each thread of gevent needs it's own not shared connection and cache
2617 # because each thread of gevent needs it's own not shared connection and cache
2612 # we also alter `args` so the cache key is individual for every green thread.
2618 # we also alter `args` so the cache key is individual for every green thread.
2613 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2619 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2614 inv_context_manager = rc_cache.InvalidationContext(key=repo_namespace_key, thread_scoped=True)
2620 inv_context_manager = rc_cache.InvalidationContext(key=repo_namespace_key, thread_scoped=True)
2615
2621
2616 # our wrapped caching function that takes state_uid to save the previous state in
2622 # our wrapped caching function that takes state_uid to save the previous state in
2617 def cache_generator(_state_uid):
2623 def cache_generator(_state_uid):
2618
2624
2619 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2625 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2620 def get_instance_cached(_repo_id, _process_context_id):
2626 def get_instance_cached(_repo_id, _process_context_id):
2621 # we save in cached func the generation state so we can detect a change and invalidate caches
2627 # we save in cached func the generation state so we can detect a change and invalidate caches
2622 return _state_uid, self._get_instance(repo_state_uid=_state_uid)
2628 return _state_uid, self._get_instance(repo_state_uid=_state_uid)
2623
2629
2624 return get_instance_cached
2630 return get_instance_cached
2625
2631
2626 with inv_context_manager as invalidation_context:
2632 with inv_context_manager as invalidation_context:
2627 cache_state_uid = invalidation_context.state_uid
2633 cache_state_uid = invalidation_context.state_uid
2628 cache_func = cache_generator(cache_state_uid)
2634 cache_func = cache_generator(cache_state_uid)
2629
2635
2630 args = self.repo_id, inv_context_manager.proc_key
2636 args = self.repo_id, inv_context_manager.proc_key
2631
2637
2632 previous_state_uid, instance = cache_func(*args)
2638 previous_state_uid, instance = cache_func(*args)
2633
2639
2634 # now compare keys, the "cache" state vs expected state.
2640 # now compare keys, the "cache" state vs expected state.
2635 if previous_state_uid != cache_state_uid:
2641 if previous_state_uid != cache_state_uid:
2636 log.warning('Cached state uid %s is different than current state uid %s',
2642 log.warning('Cached state uid %s is different than current state uid %s',
2637 previous_state_uid, cache_state_uid)
2643 previous_state_uid, cache_state_uid)
2638 _, instance = cache_func.refresh(*args)
2644 _, instance = cache_func.refresh(*args)
2639
2645
2640 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2646 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2641 return instance
2647 return instance
2642
2648
2643 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2649 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2644 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2650 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2645 self.repo_type, self.repo_path, cache)
2651 self.repo_type, self.repo_path, cache)
2646 config = config or self._config
2652 config = config or self._config
2647 custom_wire = {
2653 custom_wire = {
2648 'cache': cache, # controls the vcs.remote cache
2654 'cache': cache, # controls the vcs.remote cache
2649 'repo_state_uid': repo_state_uid
2655 'repo_state_uid': repo_state_uid
2650 }
2656 }
2651
2657
2652 repo = get_vcs_instance(
2658 repo = get_vcs_instance(
2653 repo_path=safe_str(self.repo_full_path),
2659 repo_path=safe_str(self.repo_full_path),
2654 config=config,
2660 config=config,
2655 with_wire=custom_wire,
2661 with_wire=custom_wire,
2656 create=False,
2662 create=False,
2657 _vcs_alias=self.repo_type)
2663 _vcs_alias=self.repo_type)
2658 if repo is not None:
2664 if repo is not None:
2659 repo.count() # cache rebuild
2665 repo.count() # cache rebuild
2660
2666
2661 return repo
2667 return repo
2662
2668
2663 def get_shadow_repository_path(self, workspace_id):
2669 def get_shadow_repository_path(self, workspace_id):
2664 from rhodecode.lib.vcs.backends.base import BaseRepository
2670 from rhodecode.lib.vcs.backends.base import BaseRepository
2665 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2671 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2666 self.repo_full_path, self.repo_id, workspace_id)
2672 self.repo_full_path, self.repo_id, workspace_id)
2667 return shadow_repo_path
2673 return shadow_repo_path
2668
2674
2669 def __json__(self):
2675 def __json__(self):
2670 return {'landing_rev': self.landing_rev}
2676 return {'landing_rev': self.landing_rev}
2671
2677
2672 def get_dict(self):
2678 def get_dict(self):
2673
2679
2674 # Since we transformed `repo_name` to a hybrid property, we need to
2680 # Since we transformed `repo_name` to a hybrid property, we need to
2675 # keep compatibility with the code which uses `repo_name` field.
2681 # keep compatibility with the code which uses `repo_name` field.
2676
2682
2677 result = super(Repository, self).get_dict()
2683 result = super(Repository, self).get_dict()
2678 result['repo_name'] = result.pop('_repo_name', None)
2684 result['repo_name'] = result.pop('_repo_name', None)
2679 result.pop('_changeset_cache', '')
2685 result.pop('_changeset_cache', '')
2680 return result
2686 return result
2681
2687
2682
2688
2683 class RepoGroup(Base, BaseModel):
2689 class RepoGroup(Base, BaseModel):
2684 __tablename__ = 'groups'
2690 __tablename__ = 'groups'
2685 __table_args__ = (
2691 __table_args__ = (
2686 UniqueConstraint('group_name', 'group_parent_id'),
2692 UniqueConstraint('group_name', 'group_parent_id'),
2687 base_table_args,
2693 base_table_args,
2688 )
2694 )
2689
2695
2690 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2696 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2691
2697
2692 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2698 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2693 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2699 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2694 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2700 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2695 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2701 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2696 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2702 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2697 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2703 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2698 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2704 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2699 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2705 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2700 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2706 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2701 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2707 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2702 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2708 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2703
2709
2704 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id', back_populates='group')
2710 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id', back_populates='group')
2705 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='group')
2711 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='group')
2706 parent_group = relationship('RepoGroup', remote_side=group_id)
2712 parent_group = relationship('RepoGroup', remote_side=group_id)
2707 user = relationship('User', back_populates='repository_groups')
2713 user = relationship('User', back_populates='repository_groups')
2708 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo_group')
2714 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo_group')
2709
2715
2710 # no cascade, set NULL
2716 # no cascade, set NULL
2711 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id', viewonly=True)
2717 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id', viewonly=True)
2712
2718
2713 def __init__(self, group_name='', parent_group=None):
2719 def __init__(self, group_name='', parent_group=None):
2714 self.group_name = group_name
2720 self.group_name = group_name
2715 self.parent_group = parent_group
2721 self.parent_group = parent_group
2716
2722
2717 def __repr__(self):
2723 def __repr__(self):
2718 return f"<{self.cls_name}('id:{self.group_id}:{self.group_name}')>"
2724 return f"<{self.cls_name}('id:{self.group_id}:{self.group_name}')>"
2719
2725
2720 @hybrid_property
2726 @hybrid_property
2721 def group_name(self):
2727 def group_name(self):
2722 return self._group_name
2728 return self._group_name
2723
2729
2724 @group_name.setter
2730 @group_name.setter
2725 def group_name(self, value):
2731 def group_name(self, value):
2726 self._group_name = value
2732 self._group_name = value
2727 self.group_name_hash = self.hash_repo_group_name(value)
2733 self.group_name_hash = self.hash_repo_group_name(value)
2728
2734
2729 @classmethod
2735 @classmethod
2730 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2736 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2731 from rhodecode.lib.vcs.backends.base import EmptyCommit
2737 from rhodecode.lib.vcs.backends.base import EmptyCommit
2732 dummy = EmptyCommit().__json__()
2738 dummy = EmptyCommit().__json__()
2733 if not changeset_cache_raw:
2739 if not changeset_cache_raw:
2734 dummy['source_repo_id'] = repo_id
2740 dummy['source_repo_id'] = repo_id
2735 return json.loads(json.dumps(dummy))
2741 return json.loads(json.dumps(dummy))
2736
2742
2737 try:
2743 try:
2738 return json.loads(changeset_cache_raw)
2744 return json.loads(changeset_cache_raw)
2739 except TypeError:
2745 except TypeError:
2740 return dummy
2746 return dummy
2741 except Exception:
2747 except Exception:
2742 log.error(traceback.format_exc())
2748 log.error(traceback.format_exc())
2743 return dummy
2749 return dummy
2744
2750
2745 @hybrid_property
2751 @hybrid_property
2746 def changeset_cache(self):
2752 def changeset_cache(self):
2747 return self._load_changeset_cache('', self._changeset_cache)
2753 return self._load_changeset_cache('', self._changeset_cache)
2748
2754
2749 @changeset_cache.setter
2755 @changeset_cache.setter
2750 def changeset_cache(self, val):
2756 def changeset_cache(self, val):
2751 try:
2757 try:
2752 self._changeset_cache = json.dumps(val)
2758 self._changeset_cache = json.dumps(val)
2753 except Exception:
2759 except Exception:
2754 log.error(traceback.format_exc())
2760 log.error(traceback.format_exc())
2755
2761
2756 @validates('group_parent_id')
2762 @validates('group_parent_id')
2757 def validate_group_parent_id(self, key, val):
2763 def validate_group_parent_id(self, key, val):
2758 """
2764 """
2759 Check cycle references for a parent group to self
2765 Check cycle references for a parent group to self
2760 """
2766 """
2761 if self.group_id and val:
2767 if self.group_id and val:
2762 assert val != self.group_id
2768 assert val != self.group_id
2763
2769
2764 return val
2770 return val
2765
2771
2766 @hybrid_property
2772 @hybrid_property
2767 def description_safe(self):
2773 def description_safe(self):
2768 from rhodecode.lib import helpers as h
2774 from rhodecode.lib import helpers as h
2769 return h.escape(self.group_description)
2775 return h.escape(self.group_description)
2770
2776
2771 @classmethod
2777 @classmethod
2772 def hash_repo_group_name(cls, repo_group_name):
2778 def hash_repo_group_name(cls, repo_group_name):
2773 val = remove_formatting(repo_group_name)
2779 val = remove_formatting(repo_group_name)
2774 val = safe_str(val).lower()
2780 val = safe_str(val).lower()
2775 chars = []
2781 chars = []
2776 for c in val:
2782 for c in val:
2777 if c not in string.ascii_letters:
2783 if c not in string.ascii_letters:
2778 c = str(ord(c))
2784 c = str(ord(c))
2779 chars.append(c)
2785 chars.append(c)
2780
2786
2781 return ''.join(chars)
2787 return ''.join(chars)
2782
2788
2783 @classmethod
2789 @classmethod
2784 def _generate_choice(cls, repo_group):
2790 def _generate_choice(cls, repo_group):
2785 from webhelpers2.html import literal as _literal
2791 from webhelpers2.html import literal as _literal
2786
2792
2787 def _name(k):
2793 def _name(k):
2788 return _literal(cls.CHOICES_SEPARATOR.join(k))
2794 return _literal(cls.CHOICES_SEPARATOR.join(k))
2789
2795
2790 return repo_group.group_id, _name(repo_group.full_path_splitted)
2796 return repo_group.group_id, _name(repo_group.full_path_splitted)
2791
2797
2792 @classmethod
2798 @classmethod
2793 def groups_choices(cls, groups=None, show_empty_group=True):
2799 def groups_choices(cls, groups=None, show_empty_group=True):
2794 if not groups:
2800 if not groups:
2795 groups = cls.query().all()
2801 groups = cls.query().all()
2796
2802
2797 repo_groups = []
2803 repo_groups = []
2798 if show_empty_group:
2804 if show_empty_group:
2799 repo_groups = [(-1, '-- %s --' % _('No parent'))]
2805 repo_groups = [(-1, '-- %s --' % _('No parent'))]
2800
2806
2801 repo_groups.extend([cls._generate_choice(x) for x in groups])
2807 repo_groups.extend([cls._generate_choice(x) for x in groups])
2802
2808
2803 repo_groups = sorted(
2809 repo_groups = sorted(
2804 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2810 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2805 return repo_groups
2811 return repo_groups
2806
2812
2807 @classmethod
2813 @classmethod
2808 def url_sep(cls):
2814 def url_sep(cls):
2809 return URL_SEP
2815 return URL_SEP
2810
2816
2811 @classmethod
2817 @classmethod
2812 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2818 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2813 if case_insensitive:
2819 if case_insensitive:
2814 gr = cls.query().filter(func.lower(cls.group_name)
2820 gr = cls.query().filter(func.lower(cls.group_name)
2815 == func.lower(group_name))
2821 == func.lower(group_name))
2816 else:
2822 else:
2817 gr = cls.query().filter(cls.group_name == group_name)
2823 gr = cls.query().filter(cls.group_name == group_name)
2818 if cache:
2824 if cache:
2819 name_key = _hash_key(group_name)
2825 name_key = _hash_key(group_name)
2820 gr = gr.options(
2826 gr = gr.options(
2821 FromCache("sql_cache_short", f"get_group_{name_key}"))
2827 FromCache("sql_cache_short", f"get_group_{name_key}"))
2822 return gr.scalar()
2828 return gr.scalar()
2823
2829
2824 @classmethod
2830 @classmethod
2825 def get_user_personal_repo_group(cls, user_id):
2831 def get_user_personal_repo_group(cls, user_id):
2826 user = User.get(user_id)
2832 user = User.get(user_id)
2827 if user.username == User.DEFAULT_USER:
2833 if user.username == User.DEFAULT_USER:
2828 return None
2834 return None
2829
2835
2830 return cls.query()\
2836 return cls.query()\
2831 .filter(cls.personal == true()) \
2837 .filter(cls.personal == true()) \
2832 .filter(cls.user == user) \
2838 .filter(cls.user == user) \
2833 .order_by(cls.group_id.asc()) \
2839 .order_by(cls.group_id.asc()) \
2834 .first()
2840 .first()
2835
2841
2836 @classmethod
2842 @classmethod
2837 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2843 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2838 case_insensitive=True):
2844 case_insensitive=True):
2839 q = RepoGroup.query()
2845 q = RepoGroup.query()
2840
2846
2841 if not isinstance(user_id, Optional):
2847 if not isinstance(user_id, Optional):
2842 q = q.filter(RepoGroup.user_id == user_id)
2848 q = q.filter(RepoGroup.user_id == user_id)
2843
2849
2844 if not isinstance(group_id, Optional):
2850 if not isinstance(group_id, Optional):
2845 q = q.filter(RepoGroup.group_parent_id == group_id)
2851 q = q.filter(RepoGroup.group_parent_id == group_id)
2846
2852
2847 if case_insensitive:
2853 if case_insensitive:
2848 q = q.order_by(func.lower(RepoGroup.group_name))
2854 q = q.order_by(func.lower(RepoGroup.group_name))
2849 else:
2855 else:
2850 q = q.order_by(RepoGroup.group_name)
2856 q = q.order_by(RepoGroup.group_name)
2851 return q.all()
2857 return q.all()
2852
2858
2853 @property
2859 @property
2854 def parents(self, parents_recursion_limit=10):
2860 def parents(self, parents_recursion_limit=10):
2855 groups = []
2861 groups = []
2856 if self.parent_group is None:
2862 if self.parent_group is None:
2857 return groups
2863 return groups
2858 cur_gr = self.parent_group
2864 cur_gr = self.parent_group
2859 groups.insert(0, cur_gr)
2865 groups.insert(0, cur_gr)
2860 cnt = 0
2866 cnt = 0
2861 while 1:
2867 while 1:
2862 cnt += 1
2868 cnt += 1
2863 gr = getattr(cur_gr, 'parent_group', None)
2869 gr = getattr(cur_gr, 'parent_group', None)
2864 cur_gr = cur_gr.parent_group
2870 cur_gr = cur_gr.parent_group
2865 if gr is None:
2871 if gr is None:
2866 break
2872 break
2867 if cnt == parents_recursion_limit:
2873 if cnt == parents_recursion_limit:
2868 # this will prevent accidental infinit loops
2874 # this will prevent accidental infinit loops
2869 log.error('more than %s parents found for group %s, stopping '
2875 log.error('more than %s parents found for group %s, stopping '
2870 'recursive parent fetching', parents_recursion_limit, self)
2876 'recursive parent fetching', parents_recursion_limit, self)
2871 break
2877 break
2872
2878
2873 groups.insert(0, gr)
2879 groups.insert(0, gr)
2874 return groups
2880 return groups
2875
2881
2876 @property
2882 @property
2877 def last_commit_cache_update_diff(self):
2883 def last_commit_cache_update_diff(self):
2878 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2884 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2879
2885
2880 @classmethod
2886 @classmethod
2881 def _load_commit_change(cls, last_commit_cache):
2887 def _load_commit_change(cls, last_commit_cache):
2882 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2888 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2883 empty_date = datetime.datetime.fromtimestamp(0)
2889 empty_date = datetime.datetime.fromtimestamp(0)
2884 date_latest = last_commit_cache.get('date', empty_date)
2890 date_latest = last_commit_cache.get('date', empty_date)
2885 try:
2891 try:
2886 return parse_datetime(date_latest)
2892 return parse_datetime(date_latest)
2887 except Exception:
2893 except Exception:
2888 return empty_date
2894 return empty_date
2889
2895
2890 @property
2896 @property
2891 def last_commit_change(self):
2897 def last_commit_change(self):
2892 return self._load_commit_change(self.changeset_cache)
2898 return self._load_commit_change(self.changeset_cache)
2893
2899
2894 @property
2900 @property
2895 def last_db_change(self):
2901 def last_db_change(self):
2896 return self.updated_on
2902 return self.updated_on
2897
2903
2898 @property
2904 @property
2899 def children(self):
2905 def children(self):
2900 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2906 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2901
2907
2902 @property
2908 @property
2903 def name(self):
2909 def name(self):
2904 return self.group_name.split(RepoGroup.url_sep())[-1]
2910 return self.group_name.split(RepoGroup.url_sep())[-1]
2905
2911
2906 @property
2912 @property
2907 def full_path(self):
2913 def full_path(self):
2908 return self.group_name
2914 return self.group_name
2909
2915
2910 @property
2916 @property
2911 def full_path_splitted(self):
2917 def full_path_splitted(self):
2912 return self.group_name.split(RepoGroup.url_sep())
2918 return self.group_name.split(RepoGroup.url_sep())
2913
2919
2914 @property
2920 @property
2915 def repositories(self):
2921 def repositories(self):
2916 return Repository.query()\
2922 return Repository.query()\
2917 .filter(Repository.group == self)\
2923 .filter(Repository.group == self)\
2918 .order_by(Repository.repo_name)
2924 .order_by(Repository.repo_name)
2919
2925
2920 @property
2926 @property
2921 def repositories_recursive_count(self):
2927 def repositories_recursive_count(self):
2922 cnt = self.repositories.count()
2928 cnt = self.repositories.count()
2923
2929
2924 def children_count(group):
2930 def children_count(group):
2925 cnt = 0
2931 cnt = 0
2926 for child in group.children:
2932 for child in group.children:
2927 cnt += child.repositories.count()
2933 cnt += child.repositories.count()
2928 cnt += children_count(child)
2934 cnt += children_count(child)
2929 return cnt
2935 return cnt
2930
2936
2931 return cnt + children_count(self)
2937 return cnt + children_count(self)
2932
2938
2933 def _recursive_objects(self, include_repos=True, include_groups=True):
2939 def _recursive_objects(self, include_repos=True, include_groups=True):
2934 all_ = []
2940 all_ = []
2935
2941
2936 def _get_members(root_gr):
2942 def _get_members(root_gr):
2937 if include_repos:
2943 if include_repos:
2938 for r in root_gr.repositories:
2944 for r in root_gr.repositories:
2939 all_.append(r)
2945 all_.append(r)
2940 childs = root_gr.children.all()
2946 childs = root_gr.children.all()
2941 if childs:
2947 if childs:
2942 for gr in childs:
2948 for gr in childs:
2943 if include_groups:
2949 if include_groups:
2944 all_.append(gr)
2950 all_.append(gr)
2945 _get_members(gr)
2951 _get_members(gr)
2946
2952
2947 root_group = []
2953 root_group = []
2948 if include_groups:
2954 if include_groups:
2949 root_group = [self]
2955 root_group = [self]
2950
2956
2951 _get_members(self)
2957 _get_members(self)
2952 return root_group + all_
2958 return root_group + all_
2953
2959
2954 def recursive_groups_and_repos(self):
2960 def recursive_groups_and_repos(self):
2955 """
2961 """
2956 Recursive return all groups, with repositories in those groups
2962 Recursive return all groups, with repositories in those groups
2957 """
2963 """
2958 return self._recursive_objects()
2964 return self._recursive_objects()
2959
2965
2960 def recursive_groups(self):
2966 def recursive_groups(self):
2961 """
2967 """
2962 Returns all children groups for this group including children of children
2968 Returns all children groups for this group including children of children
2963 """
2969 """
2964 return self._recursive_objects(include_repos=False)
2970 return self._recursive_objects(include_repos=False)
2965
2971
2966 def recursive_repos(self):
2972 def recursive_repos(self):
2967 """
2973 """
2968 Returns all children repositories for this group
2974 Returns all children repositories for this group
2969 """
2975 """
2970 return self._recursive_objects(include_groups=False)
2976 return self._recursive_objects(include_groups=False)
2971
2977
2972 def get_new_name(self, group_name):
2978 def get_new_name(self, group_name):
2973 """
2979 """
2974 returns new full group name based on parent and new name
2980 returns new full group name based on parent and new name
2975
2981
2976 :param group_name:
2982 :param group_name:
2977 """
2983 """
2978 path_prefix = (self.parent_group.full_path_splitted if
2984 path_prefix = (self.parent_group.full_path_splitted if
2979 self.parent_group else [])
2985 self.parent_group else [])
2980 return RepoGroup.url_sep().join(path_prefix + [group_name])
2986 return RepoGroup.url_sep().join(path_prefix + [group_name])
2981
2987
2982 def update_commit_cache(self, config=None):
2988 def update_commit_cache(self, config=None):
2983 """
2989 """
2984 Update cache of last commit for newest repository inside this repository group.
2990 Update cache of last commit for newest repository inside this repository group.
2985 cache_keys should be::
2991 cache_keys should be::
2986
2992
2987 source_repo_id
2993 source_repo_id
2988 short_id
2994 short_id
2989 raw_id
2995 raw_id
2990 revision
2996 revision
2991 parents
2997 parents
2992 message
2998 message
2993 date
2999 date
2994 author
3000 author
2995
3001
2996 """
3002 """
2997 from rhodecode.lib.vcs.utils.helpers import parse_datetime
3003 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2998 empty_date = datetime.datetime.fromtimestamp(0)
3004 empty_date = datetime.datetime.fromtimestamp(0)
2999
3005
3000 def repo_groups_and_repos(root_gr):
3006 def repo_groups_and_repos(root_gr):
3001 for _repo in root_gr.repositories:
3007 for _repo in root_gr.repositories:
3002 yield _repo
3008 yield _repo
3003 for child_group in root_gr.children.all():
3009 for child_group in root_gr.children.all():
3004 yield child_group
3010 yield child_group
3005
3011
3006 latest_repo_cs_cache = {}
3012 latest_repo_cs_cache = {}
3007 for obj in repo_groups_and_repos(self):
3013 for obj in repo_groups_and_repos(self):
3008 repo_cs_cache = obj.changeset_cache
3014 repo_cs_cache = obj.changeset_cache
3009 date_latest = latest_repo_cs_cache.get('date', empty_date)
3015 date_latest = latest_repo_cs_cache.get('date', empty_date)
3010 date_current = repo_cs_cache.get('date', empty_date)
3016 date_current = repo_cs_cache.get('date', empty_date)
3011 current_timestamp = datetime_to_time(parse_datetime(date_latest))
3017 current_timestamp = datetime_to_time(parse_datetime(date_latest))
3012 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
3018 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
3013 latest_repo_cs_cache = repo_cs_cache
3019 latest_repo_cs_cache = repo_cs_cache
3014 if hasattr(obj, 'repo_id'):
3020 if hasattr(obj, 'repo_id'):
3015 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
3021 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
3016 else:
3022 else:
3017 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
3023 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
3018
3024
3019 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
3025 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
3020
3026
3021 latest_repo_cs_cache['updated_on'] = time.time()
3027 latest_repo_cs_cache['updated_on'] = time.time()
3022 self.changeset_cache = latest_repo_cs_cache
3028 self.changeset_cache = latest_repo_cs_cache
3023 self.updated_on = _date_latest
3029 self.updated_on = _date_latest
3024 Session().add(self)
3030 Session().add(self)
3025 Session().commit()
3031 Session().commit()
3026
3032
3027 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
3033 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
3028 self.group_name, latest_repo_cs_cache, _date_latest)
3034 self.group_name, latest_repo_cs_cache, _date_latest)
3029
3035
3030 def permissions(self, with_admins=True, with_owner=True,
3036 def permissions(self, with_admins=True, with_owner=True,
3031 expand_from_user_groups=False):
3037 expand_from_user_groups=False):
3032 """
3038 """
3033 Permissions for repository groups
3039 Permissions for repository groups
3034 """
3040 """
3035 _admin_perm = 'group.admin'
3041 _admin_perm = 'group.admin'
3036
3042
3037 owner_row = []
3043 owner_row = []
3038 if with_owner:
3044 if with_owner:
3039 usr = AttributeDict(self.user.get_dict())
3045 usr = AttributeDict(self.user.get_dict())
3040 usr.owner_row = True
3046 usr.owner_row = True
3041 usr.permission = _admin_perm
3047 usr.permission = _admin_perm
3042 owner_row.append(usr)
3048 owner_row.append(usr)
3043
3049
3044 super_admin_ids = []
3050 super_admin_ids = []
3045 super_admin_rows = []
3051 super_admin_rows = []
3046 if with_admins:
3052 if with_admins:
3047 for usr in User.get_all_super_admins():
3053 for usr in User.get_all_super_admins():
3048 super_admin_ids.append(usr.user_id)
3054 super_admin_ids.append(usr.user_id)
3049 # if this admin is also owner, don't double the record
3055 # if this admin is also owner, don't double the record
3050 if usr.user_id == owner_row[0].user_id:
3056 if usr.user_id == owner_row[0].user_id:
3051 owner_row[0].admin_row = True
3057 owner_row[0].admin_row = True
3052 else:
3058 else:
3053 usr = AttributeDict(usr.get_dict())
3059 usr = AttributeDict(usr.get_dict())
3054 usr.admin_row = True
3060 usr.admin_row = True
3055 usr.permission = _admin_perm
3061 usr.permission = _admin_perm
3056 super_admin_rows.append(usr)
3062 super_admin_rows.append(usr)
3057
3063
3058 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3064 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3059 q = q.options(joinedload(UserRepoGroupToPerm.group),
3065 q = q.options(joinedload(UserRepoGroupToPerm.group),
3060 joinedload(UserRepoGroupToPerm.user),
3066 joinedload(UserRepoGroupToPerm.user),
3061 joinedload(UserRepoGroupToPerm.permission),)
3067 joinedload(UserRepoGroupToPerm.permission),)
3062
3068
3063 # get owners and admins and permissions. We do a trick of re-writing
3069 # get owners and admins and permissions. We do a trick of re-writing
3064 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3070 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3065 # has a global reference and changing one object propagates to all
3071 # has a global reference and changing one object propagates to all
3066 # others. This means if admin is also an owner admin_row that change
3072 # others. This means if admin is also an owner admin_row that change
3067 # would propagate to both objects
3073 # would propagate to both objects
3068 perm_rows = []
3074 perm_rows = []
3069 for _usr in q.all():
3075 for _usr in q.all():
3070 usr = AttributeDict(_usr.user.get_dict())
3076 usr = AttributeDict(_usr.user.get_dict())
3071 # if this user is also owner/admin, mark as duplicate record
3077 # if this user is also owner/admin, mark as duplicate record
3072 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3078 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3073 usr.duplicate_perm = True
3079 usr.duplicate_perm = True
3074 usr.permission = _usr.permission.permission_name
3080 usr.permission = _usr.permission.permission_name
3075 perm_rows.append(usr)
3081 perm_rows.append(usr)
3076
3082
3077 # filter the perm rows by 'default' first and then sort them by
3083 # filter the perm rows by 'default' first and then sort them by
3078 # admin,write,read,none permissions sorted again alphabetically in
3084 # admin,write,read,none permissions sorted again alphabetically in
3079 # each group
3085 # each group
3080 perm_rows = sorted(perm_rows, key=display_user_sort)
3086 perm_rows = sorted(perm_rows, key=display_user_sort)
3081
3087
3082 user_groups_rows = []
3088 user_groups_rows = []
3083 if expand_from_user_groups:
3089 if expand_from_user_groups:
3084 for ug in self.permission_user_groups(with_members=True):
3090 for ug in self.permission_user_groups(with_members=True):
3085 for user_data in ug.members:
3091 for user_data in ug.members:
3086 user_groups_rows.append(user_data)
3092 user_groups_rows.append(user_data)
3087
3093
3088 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3094 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3089
3095
3090 def permission_user_groups(self, with_members=False):
3096 def permission_user_groups(self, with_members=False):
3091 q = UserGroupRepoGroupToPerm.query()\
3097 q = UserGroupRepoGroupToPerm.query()\
3092 .filter(UserGroupRepoGroupToPerm.group == self)
3098 .filter(UserGroupRepoGroupToPerm.group == self)
3093 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3099 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3094 joinedload(UserGroupRepoGroupToPerm.users_group),
3100 joinedload(UserGroupRepoGroupToPerm.users_group),
3095 joinedload(UserGroupRepoGroupToPerm.permission),)
3101 joinedload(UserGroupRepoGroupToPerm.permission),)
3096
3102
3097 perm_rows = []
3103 perm_rows = []
3098 for _user_group in q.all():
3104 for _user_group in q.all():
3099 entry = AttributeDict(_user_group.users_group.get_dict())
3105 entry = AttributeDict(_user_group.users_group.get_dict())
3100 entry.permission = _user_group.permission.permission_name
3106 entry.permission = _user_group.permission.permission_name
3101 if with_members:
3107 if with_members:
3102 entry.members = [x.user.get_dict()
3108 entry.members = [x.user.get_dict()
3103 for x in _user_group.users_group.members]
3109 for x in _user_group.users_group.members]
3104 perm_rows.append(entry)
3110 perm_rows.append(entry)
3105
3111
3106 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3112 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3107 return perm_rows
3113 return perm_rows
3108
3114
3109 def get_api_data(self):
3115 def get_api_data(self):
3110 """
3116 """
3111 Common function for generating api data
3117 Common function for generating api data
3112
3118
3113 """
3119 """
3114 group = self
3120 group = self
3115 data = {
3121 data = {
3116 'group_id': group.group_id,
3122 'group_id': group.group_id,
3117 'group_name': group.group_name,
3123 'group_name': group.group_name,
3118 'group_description': group.description_safe,
3124 'group_description': group.description_safe,
3119 'parent_group': group.parent_group.group_name if group.parent_group else None,
3125 'parent_group': group.parent_group.group_name if group.parent_group else None,
3120 'repositories': [x.repo_name for x in group.repositories],
3126 'repositories': [x.repo_name for x in group.repositories],
3121 'owner': group.user.username,
3127 'owner': group.user.username,
3122 }
3128 }
3123 return data
3129 return data
3124
3130
3125 def get_dict(self):
3131 def get_dict(self):
3126 # Since we transformed `group_name` to a hybrid property, we need to
3132 # Since we transformed `group_name` to a hybrid property, we need to
3127 # keep compatibility with the code which uses `group_name` field.
3133 # keep compatibility with the code which uses `group_name` field.
3128 result = super(RepoGroup, self).get_dict()
3134 result = super(RepoGroup, self).get_dict()
3129 result['group_name'] = result.pop('_group_name', None)
3135 result['group_name'] = result.pop('_group_name', None)
3130 result.pop('_changeset_cache', '')
3136 result.pop('_changeset_cache', '')
3131 return result
3137 return result
3132
3138
3133
3139
3134 class Permission(Base, BaseModel):
3140 class Permission(Base, BaseModel):
3135 __tablename__ = 'permissions'
3141 __tablename__ = 'permissions'
3136 __table_args__ = (
3142 __table_args__ = (
3137 Index('p_perm_name_idx', 'permission_name'),
3143 Index('p_perm_name_idx', 'permission_name'),
3138 base_table_args,
3144 base_table_args,
3139 )
3145 )
3140
3146
3141 PERMS = [
3147 PERMS = [
3142 ('hg.admin', _('RhodeCode Super Administrator')),
3148 ('hg.admin', _('RhodeCode Super Administrator')),
3143
3149
3144 ('repository.none', _('Repository no access')),
3150 ('repository.none', _('Repository no access')),
3145 ('repository.read', _('Repository read access')),
3151 ('repository.read', _('Repository read access')),
3146 ('repository.write', _('Repository write access')),
3152 ('repository.write', _('Repository write access')),
3147 ('repository.admin', _('Repository admin access')),
3153 ('repository.admin', _('Repository admin access')),
3148
3154
3149 ('group.none', _('Repository group no access')),
3155 ('group.none', _('Repository group no access')),
3150 ('group.read', _('Repository group read access')),
3156 ('group.read', _('Repository group read access')),
3151 ('group.write', _('Repository group write access')),
3157 ('group.write', _('Repository group write access')),
3152 ('group.admin', _('Repository group admin access')),
3158 ('group.admin', _('Repository group admin access')),
3153
3159
3154 ('usergroup.none', _('User group no access')),
3160 ('usergroup.none', _('User group no access')),
3155 ('usergroup.read', _('User group read access')),
3161 ('usergroup.read', _('User group read access')),
3156 ('usergroup.write', _('User group write access')),
3162 ('usergroup.write', _('User group write access')),
3157 ('usergroup.admin', _('User group admin access')),
3163 ('usergroup.admin', _('User group admin access')),
3158
3164
3159 ('branch.none', _('Branch no permissions')),
3165 ('branch.none', _('Branch no permissions')),
3160 ('branch.merge', _('Branch access by web merge')),
3166 ('branch.merge', _('Branch access by web merge')),
3161 ('branch.push', _('Branch access by push')),
3167 ('branch.push', _('Branch access by push')),
3162 ('branch.push_force', _('Branch access by push with force')),
3168 ('branch.push_force', _('Branch access by push with force')),
3163
3169
3164 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3170 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3165 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3171 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3166
3172
3167 ('hg.usergroup.create.false', _('User Group creation disabled')),
3173 ('hg.usergroup.create.false', _('User Group creation disabled')),
3168 ('hg.usergroup.create.true', _('User Group creation enabled')),
3174 ('hg.usergroup.create.true', _('User Group creation enabled')),
3169
3175
3170 ('hg.create.none', _('Repository creation disabled')),
3176 ('hg.create.none', _('Repository creation disabled')),
3171 ('hg.create.repository', _('Repository creation enabled')),
3177 ('hg.create.repository', _('Repository creation enabled')),
3172 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3178 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3173 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3179 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3174
3180
3175 ('hg.fork.none', _('Repository forking disabled')),
3181 ('hg.fork.none', _('Repository forking disabled')),
3176 ('hg.fork.repository', _('Repository forking enabled')),
3182 ('hg.fork.repository', _('Repository forking enabled')),
3177
3183
3178 ('hg.register.none', _('Registration disabled')),
3184 ('hg.register.none', _('Registration disabled')),
3179 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3185 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3180 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3186 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3181
3187
3182 ('hg.password_reset.enabled', _('Password reset enabled')),
3188 ('hg.password_reset.enabled', _('Password reset enabled')),
3183 ('hg.password_reset.hidden', _('Password reset hidden')),
3189 ('hg.password_reset.hidden', _('Password reset hidden')),
3184 ('hg.password_reset.disabled', _('Password reset disabled')),
3190 ('hg.password_reset.disabled', _('Password reset disabled')),
3185
3191
3186 ('hg.extern_activate.manual', _('Manual activation of external account')),
3192 ('hg.extern_activate.manual', _('Manual activation of external account')),
3187 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3193 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3188
3194
3189 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3195 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3190 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3196 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3191 ]
3197 ]
3192
3198
3193 # definition of system default permissions for DEFAULT user, created on
3199 # definition of system default permissions for DEFAULT user, created on
3194 # system setup
3200 # system setup
3195 DEFAULT_USER_PERMISSIONS = [
3201 DEFAULT_USER_PERMISSIONS = [
3196 # object perms
3202 # object perms
3197 'repository.read',
3203 'repository.read',
3198 'group.read',
3204 'group.read',
3199 'usergroup.read',
3205 'usergroup.read',
3200 # branch, for backward compat we need same value as before so forced pushed
3206 # branch, for backward compat we need same value as before so forced pushed
3201 'branch.push_force',
3207 'branch.push_force',
3202 # global
3208 # global
3203 'hg.create.repository',
3209 'hg.create.repository',
3204 'hg.repogroup.create.false',
3210 'hg.repogroup.create.false',
3205 'hg.usergroup.create.false',
3211 'hg.usergroup.create.false',
3206 'hg.create.write_on_repogroup.true',
3212 'hg.create.write_on_repogroup.true',
3207 'hg.fork.repository',
3213 'hg.fork.repository',
3208 'hg.register.manual_activate',
3214 'hg.register.manual_activate',
3209 'hg.password_reset.enabled',
3215 'hg.password_reset.enabled',
3210 'hg.extern_activate.auto',
3216 'hg.extern_activate.auto',
3211 'hg.inherit_default_perms.true',
3217 'hg.inherit_default_perms.true',
3212 ]
3218 ]
3213
3219
3214 # defines which permissions are more important higher the more important
3220 # defines which permissions are more important higher the more important
3215 # Weight defines which permissions are more important.
3221 # Weight defines which permissions are more important.
3216 # The higher number the more important.
3222 # The higher number the more important.
3217 PERM_WEIGHTS = {
3223 PERM_WEIGHTS = {
3218 'repository.none': 0,
3224 'repository.none': 0,
3219 'repository.read': 1,
3225 'repository.read': 1,
3220 'repository.write': 3,
3226 'repository.write': 3,
3221 'repository.admin': 4,
3227 'repository.admin': 4,
3222
3228
3223 'group.none': 0,
3229 'group.none': 0,
3224 'group.read': 1,
3230 'group.read': 1,
3225 'group.write': 3,
3231 'group.write': 3,
3226 'group.admin': 4,
3232 'group.admin': 4,
3227
3233
3228 'usergroup.none': 0,
3234 'usergroup.none': 0,
3229 'usergroup.read': 1,
3235 'usergroup.read': 1,
3230 'usergroup.write': 3,
3236 'usergroup.write': 3,
3231 'usergroup.admin': 4,
3237 'usergroup.admin': 4,
3232
3238
3233 'branch.none': 0,
3239 'branch.none': 0,
3234 'branch.merge': 1,
3240 'branch.merge': 1,
3235 'branch.push': 3,
3241 'branch.push': 3,
3236 'branch.push_force': 4,
3242 'branch.push_force': 4,
3237
3243
3238 'hg.repogroup.create.false': 0,
3244 'hg.repogroup.create.false': 0,
3239 'hg.repogroup.create.true': 1,
3245 'hg.repogroup.create.true': 1,
3240
3246
3241 'hg.usergroup.create.false': 0,
3247 'hg.usergroup.create.false': 0,
3242 'hg.usergroup.create.true': 1,
3248 'hg.usergroup.create.true': 1,
3243
3249
3244 'hg.fork.none': 0,
3250 'hg.fork.none': 0,
3245 'hg.fork.repository': 1,
3251 'hg.fork.repository': 1,
3246 'hg.create.none': 0,
3252 'hg.create.none': 0,
3247 'hg.create.repository': 1
3253 'hg.create.repository': 1
3248 }
3254 }
3249
3255
3250 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3256 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3251 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3257 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3252 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3258 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3253
3259
3254 def __repr__(self):
3260 def __repr__(self):
3255 return "<%s('%s:%s')>" % (
3261 return "<%s('%s:%s')>" % (
3256 self.cls_name, self.permission_id, self.permission_name
3262 self.cls_name, self.permission_id, self.permission_name
3257 )
3263 )
3258
3264
3259 @classmethod
3265 @classmethod
3260 def get_by_key(cls, key):
3266 def get_by_key(cls, key):
3261 return cls.query().filter(cls.permission_name == key).scalar()
3267 return cls.query().filter(cls.permission_name == key).scalar()
3262
3268
3263 @classmethod
3269 @classmethod
3264 def get_default_repo_perms(cls, user_id, repo_id=None):
3270 def get_default_repo_perms(cls, user_id, repo_id=None):
3265 q = Session().query(UserRepoToPerm, Repository, Permission)\
3271 q = Session().query(UserRepoToPerm, Repository, Permission)\
3266 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3272 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3267 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3273 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3268 .filter(UserRepoToPerm.user_id == user_id)
3274 .filter(UserRepoToPerm.user_id == user_id)
3269 if repo_id:
3275 if repo_id:
3270 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3276 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3271 return q.all()
3277 return q.all()
3272
3278
3273 @classmethod
3279 @classmethod
3274 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3280 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3275 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3281 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3276 .join(
3282 .join(
3277 Permission,
3283 Permission,
3278 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3284 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3279 .join(
3285 .join(
3280 UserRepoToPerm,
3286 UserRepoToPerm,
3281 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3287 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3282 .filter(UserRepoToPerm.user_id == user_id)
3288 .filter(UserRepoToPerm.user_id == user_id)
3283
3289
3284 if repo_id:
3290 if repo_id:
3285 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3291 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3286 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3292 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3287
3293
3288 @classmethod
3294 @classmethod
3289 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3295 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3290 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3296 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3291 .join(
3297 .join(
3292 Permission,
3298 Permission,
3293 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3299 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3294 .join(
3300 .join(
3295 Repository,
3301 Repository,
3296 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3302 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3297 .join(
3303 .join(
3298 UserGroup,
3304 UserGroup,
3299 UserGroupRepoToPerm.users_group_id ==
3305 UserGroupRepoToPerm.users_group_id ==
3300 UserGroup.users_group_id)\
3306 UserGroup.users_group_id)\
3301 .join(
3307 .join(
3302 UserGroupMember,
3308 UserGroupMember,
3303 UserGroupRepoToPerm.users_group_id ==
3309 UserGroupRepoToPerm.users_group_id ==
3304 UserGroupMember.users_group_id)\
3310 UserGroupMember.users_group_id)\
3305 .filter(
3311 .filter(
3306 UserGroupMember.user_id == user_id,
3312 UserGroupMember.user_id == user_id,
3307 UserGroup.users_group_active == true())
3313 UserGroup.users_group_active == true())
3308 if repo_id:
3314 if repo_id:
3309 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3315 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3310 return q.all()
3316 return q.all()
3311
3317
3312 @classmethod
3318 @classmethod
3313 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3319 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3314 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3320 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3315 .join(
3321 .join(
3316 Permission,
3322 Permission,
3317 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3323 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3318 .join(
3324 .join(
3319 UserGroupRepoToPerm,
3325 UserGroupRepoToPerm,
3320 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3326 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3321 .join(
3327 .join(
3322 UserGroup,
3328 UserGroup,
3323 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3329 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3324 .join(
3330 .join(
3325 UserGroupMember,
3331 UserGroupMember,
3326 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3332 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3327 .filter(
3333 .filter(
3328 UserGroupMember.user_id == user_id,
3334 UserGroupMember.user_id == user_id,
3329 UserGroup.users_group_active == true())
3335 UserGroup.users_group_active == true())
3330
3336
3331 if repo_id:
3337 if repo_id:
3332 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3338 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3333 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3339 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3334
3340
3335 @classmethod
3341 @classmethod
3336 def get_default_group_perms(cls, user_id, repo_group_id=None):
3342 def get_default_group_perms(cls, user_id, repo_group_id=None):
3337 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3343 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3338 .join(
3344 .join(
3339 Permission,
3345 Permission,
3340 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3346 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3341 .join(
3347 .join(
3342 RepoGroup,
3348 RepoGroup,
3343 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3349 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3344 .filter(UserRepoGroupToPerm.user_id == user_id)
3350 .filter(UserRepoGroupToPerm.user_id == user_id)
3345 if repo_group_id:
3351 if repo_group_id:
3346 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3352 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3347 return q.all()
3353 return q.all()
3348
3354
3349 @classmethod
3355 @classmethod
3350 def get_default_group_perms_from_user_group(
3356 def get_default_group_perms_from_user_group(
3351 cls, user_id, repo_group_id=None):
3357 cls, user_id, repo_group_id=None):
3352 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3358 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3353 .join(
3359 .join(
3354 Permission,
3360 Permission,
3355 UserGroupRepoGroupToPerm.permission_id ==
3361 UserGroupRepoGroupToPerm.permission_id ==
3356 Permission.permission_id)\
3362 Permission.permission_id)\
3357 .join(
3363 .join(
3358 RepoGroup,
3364 RepoGroup,
3359 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3365 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3360 .join(
3366 .join(
3361 UserGroup,
3367 UserGroup,
3362 UserGroupRepoGroupToPerm.users_group_id ==
3368 UserGroupRepoGroupToPerm.users_group_id ==
3363 UserGroup.users_group_id)\
3369 UserGroup.users_group_id)\
3364 .join(
3370 .join(
3365 UserGroupMember,
3371 UserGroupMember,
3366 UserGroupRepoGroupToPerm.users_group_id ==
3372 UserGroupRepoGroupToPerm.users_group_id ==
3367 UserGroupMember.users_group_id)\
3373 UserGroupMember.users_group_id)\
3368 .filter(
3374 .filter(
3369 UserGroupMember.user_id == user_id,
3375 UserGroupMember.user_id == user_id,
3370 UserGroup.users_group_active == true())
3376 UserGroup.users_group_active == true())
3371 if repo_group_id:
3377 if repo_group_id:
3372 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3378 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3373 return q.all()
3379 return q.all()
3374
3380
3375 @classmethod
3381 @classmethod
3376 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3382 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3377 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3383 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3378 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3384 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3379 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3385 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3380 .filter(UserUserGroupToPerm.user_id == user_id)
3386 .filter(UserUserGroupToPerm.user_id == user_id)
3381 if user_group_id:
3387 if user_group_id:
3382 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3388 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3383 return q.all()
3389 return q.all()
3384
3390
3385 @classmethod
3391 @classmethod
3386 def get_default_user_group_perms_from_user_group(
3392 def get_default_user_group_perms_from_user_group(
3387 cls, user_id, user_group_id=None):
3393 cls, user_id, user_group_id=None):
3388 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3394 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3389 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3395 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3390 .join(
3396 .join(
3391 Permission,
3397 Permission,
3392 UserGroupUserGroupToPerm.permission_id ==
3398 UserGroupUserGroupToPerm.permission_id ==
3393 Permission.permission_id)\
3399 Permission.permission_id)\
3394 .join(
3400 .join(
3395 TargetUserGroup,
3401 TargetUserGroup,
3396 UserGroupUserGroupToPerm.target_user_group_id ==
3402 UserGroupUserGroupToPerm.target_user_group_id ==
3397 TargetUserGroup.users_group_id)\
3403 TargetUserGroup.users_group_id)\
3398 .join(
3404 .join(
3399 UserGroup,
3405 UserGroup,
3400 UserGroupUserGroupToPerm.user_group_id ==
3406 UserGroupUserGroupToPerm.user_group_id ==
3401 UserGroup.users_group_id)\
3407 UserGroup.users_group_id)\
3402 .join(
3408 .join(
3403 UserGroupMember,
3409 UserGroupMember,
3404 UserGroupUserGroupToPerm.user_group_id ==
3410 UserGroupUserGroupToPerm.user_group_id ==
3405 UserGroupMember.users_group_id)\
3411 UserGroupMember.users_group_id)\
3406 .filter(
3412 .filter(
3407 UserGroupMember.user_id == user_id,
3413 UserGroupMember.user_id == user_id,
3408 UserGroup.users_group_active == true())
3414 UserGroup.users_group_active == true())
3409 if user_group_id:
3415 if user_group_id:
3410 q = q.filter(
3416 q = q.filter(
3411 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3417 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3412
3418
3413 return q.all()
3419 return q.all()
3414
3420
3415
3421
3416 class UserRepoToPerm(Base, BaseModel):
3422 class UserRepoToPerm(Base, BaseModel):
3417 __tablename__ = 'repo_to_perm'
3423 __tablename__ = 'repo_to_perm'
3418 __table_args__ = (
3424 __table_args__ = (
3419 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3425 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3420 base_table_args
3426 base_table_args
3421 )
3427 )
3422
3428
3423 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3429 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3424 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3430 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3425 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3431 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3426 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3432 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3427
3433
3428 user = relationship('User', back_populates="repo_to_perm")
3434 user = relationship('User', back_populates="repo_to_perm")
3429 repository = relationship('Repository', back_populates="repo_to_perm")
3435 repository = relationship('Repository', back_populates="repo_to_perm")
3430 permission = relationship('Permission')
3436 permission = relationship('Permission')
3431
3437
3432 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined', back_populates='user_repo_to_perm')
3438 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined', back_populates='user_repo_to_perm')
3433
3439
3434 @classmethod
3440 @classmethod
3435 def create(cls, user, repository, permission):
3441 def create(cls, user, repository, permission):
3436 n = cls()
3442 n = cls()
3437 n.user = user
3443 n.user = user
3438 n.repository = repository
3444 n.repository = repository
3439 n.permission = permission
3445 n.permission = permission
3440 Session().add(n)
3446 Session().add(n)
3441 return n
3447 return n
3442
3448
3443 def __repr__(self):
3449 def __repr__(self):
3444 return f'<{self.user} => {self.repository} >'
3450 return f'<{self.user} => {self.repository} >'
3445
3451
3446
3452
3447 class UserUserGroupToPerm(Base, BaseModel):
3453 class UserUserGroupToPerm(Base, BaseModel):
3448 __tablename__ = 'user_user_group_to_perm'
3454 __tablename__ = 'user_user_group_to_perm'
3449 __table_args__ = (
3455 __table_args__ = (
3450 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3456 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3451 base_table_args
3457 base_table_args
3452 )
3458 )
3453
3459
3454 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3460 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3455 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3461 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3456 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3462 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3457 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3463 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3458
3464
3459 user = relationship('User', back_populates='user_group_to_perm')
3465 user = relationship('User', back_populates='user_group_to_perm')
3460 user_group = relationship('UserGroup', back_populates='user_user_group_to_perm')
3466 user_group = relationship('UserGroup', back_populates='user_user_group_to_perm')
3461 permission = relationship('Permission')
3467 permission = relationship('Permission')
3462
3468
3463 @classmethod
3469 @classmethod
3464 def create(cls, user, user_group, permission):
3470 def create(cls, user, user_group, permission):
3465 n = cls()
3471 n = cls()
3466 n.user = user
3472 n.user = user
3467 n.user_group = user_group
3473 n.user_group = user_group
3468 n.permission = permission
3474 n.permission = permission
3469 Session().add(n)
3475 Session().add(n)
3470 return n
3476 return n
3471
3477
3472 def __repr__(self):
3478 def __repr__(self):
3473 return f'<{self.user} => {self.user_group} >'
3479 return f'<{self.user} => {self.user_group} >'
3474
3480
3475
3481
3476 class UserToPerm(Base, BaseModel):
3482 class UserToPerm(Base, BaseModel):
3477 __tablename__ = 'user_to_perm'
3483 __tablename__ = 'user_to_perm'
3478 __table_args__ = (
3484 __table_args__ = (
3479 UniqueConstraint('user_id', 'permission_id'),
3485 UniqueConstraint('user_id', 'permission_id'),
3480 base_table_args
3486 base_table_args
3481 )
3487 )
3482
3488
3483 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3489 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3484 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3490 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3485 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3491 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3486
3492
3487 user = relationship('User', back_populates='user_perms')
3493 user = relationship('User', back_populates='user_perms')
3488 permission = relationship('Permission', lazy='joined')
3494 permission = relationship('Permission', lazy='joined')
3489
3495
3490 def __repr__(self):
3496 def __repr__(self):
3491 return f'<{self.user} => {self.permission} >'
3497 return f'<{self.user} => {self.permission} >'
3492
3498
3493
3499
3494 class UserGroupRepoToPerm(Base, BaseModel):
3500 class UserGroupRepoToPerm(Base, BaseModel):
3495 __tablename__ = 'users_group_repo_to_perm'
3501 __tablename__ = 'users_group_repo_to_perm'
3496 __table_args__ = (
3502 __table_args__ = (
3497 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3503 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3498 base_table_args
3504 base_table_args
3499 )
3505 )
3500
3506
3501 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3507 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3502 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3508 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3503 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3509 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3504 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3510 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3505
3511
3506 users_group = relationship('UserGroup', back_populates='users_group_repo_to_perm')
3512 users_group = relationship('UserGroup', back_populates='users_group_repo_to_perm')
3507 permission = relationship('Permission')
3513 permission = relationship('Permission')
3508 repository = relationship('Repository', back_populates='users_group_to_perm')
3514 repository = relationship('Repository', back_populates='users_group_to_perm')
3509 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all', back_populates='user_group_repo_to_perm')
3515 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all', back_populates='user_group_repo_to_perm')
3510
3516
3511 @classmethod
3517 @classmethod
3512 def create(cls, users_group, repository, permission):
3518 def create(cls, users_group, repository, permission):
3513 n = cls()
3519 n = cls()
3514 n.users_group = users_group
3520 n.users_group = users_group
3515 n.repository = repository
3521 n.repository = repository
3516 n.permission = permission
3522 n.permission = permission
3517 Session().add(n)
3523 Session().add(n)
3518 return n
3524 return n
3519
3525
3520 def __repr__(self):
3526 def __repr__(self):
3521 return f'<UserGroupRepoToPerm:{self.users_group} => {self.repository} >'
3527 return f'<UserGroupRepoToPerm:{self.users_group} => {self.repository} >'
3522
3528
3523
3529
3524 class UserGroupUserGroupToPerm(Base, BaseModel):
3530 class UserGroupUserGroupToPerm(Base, BaseModel):
3525 __tablename__ = 'user_group_user_group_to_perm'
3531 __tablename__ = 'user_group_user_group_to_perm'
3526 __table_args__ = (
3532 __table_args__ = (
3527 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3533 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3528 CheckConstraint('target_user_group_id != user_group_id'),
3534 CheckConstraint('target_user_group_id != user_group_id'),
3529 base_table_args
3535 base_table_args
3530 )
3536 )
3531
3537
3532 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)
3538 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)
3533 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3539 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3534 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3540 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3535 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3541 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3536
3542
3537 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id', back_populates='user_group_user_group_to_perm')
3543 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id', back_populates='user_group_user_group_to_perm')
3538 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3544 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3539 permission = relationship('Permission')
3545 permission = relationship('Permission')
3540
3546
3541 @classmethod
3547 @classmethod
3542 def create(cls, target_user_group, user_group, permission):
3548 def create(cls, target_user_group, user_group, permission):
3543 n = cls()
3549 n = cls()
3544 n.target_user_group = target_user_group
3550 n.target_user_group = target_user_group
3545 n.user_group = user_group
3551 n.user_group = user_group
3546 n.permission = permission
3552 n.permission = permission
3547 Session().add(n)
3553 Session().add(n)
3548 return n
3554 return n
3549
3555
3550 def __repr__(self):
3556 def __repr__(self):
3551 return f'<UserGroupUserGroup:{self.target_user_group} => {self.user_group} >'
3557 return f'<UserGroupUserGroup:{self.target_user_group} => {self.user_group} >'
3552
3558
3553
3559
3554 class UserGroupToPerm(Base, BaseModel):
3560 class UserGroupToPerm(Base, BaseModel):
3555 __tablename__ = 'users_group_to_perm'
3561 __tablename__ = 'users_group_to_perm'
3556 __table_args__ = (
3562 __table_args__ = (
3557 UniqueConstraint('users_group_id', 'permission_id',),
3563 UniqueConstraint('users_group_id', 'permission_id',),
3558 base_table_args
3564 base_table_args
3559 )
3565 )
3560
3566
3561 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3567 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3562 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3568 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3563 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3569 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3564
3570
3565 users_group = relationship('UserGroup', back_populates='users_group_to_perm')
3571 users_group = relationship('UserGroup', back_populates='users_group_to_perm')
3566 permission = relationship('Permission')
3572 permission = relationship('Permission')
3567
3573
3568
3574
3569 class UserRepoGroupToPerm(Base, BaseModel):
3575 class UserRepoGroupToPerm(Base, BaseModel):
3570 __tablename__ = 'user_repo_group_to_perm'
3576 __tablename__ = 'user_repo_group_to_perm'
3571 __table_args__ = (
3577 __table_args__ = (
3572 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3578 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3573 base_table_args
3579 base_table_args
3574 )
3580 )
3575
3581
3576 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3582 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3577 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3583 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3578 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3584 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3579 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3585 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3580
3586
3581 user = relationship('User', back_populates='repo_group_to_perm')
3587 user = relationship('User', back_populates='repo_group_to_perm')
3582 group = relationship('RepoGroup', back_populates='repo_group_to_perm')
3588 group = relationship('RepoGroup', back_populates='repo_group_to_perm')
3583 permission = relationship('Permission')
3589 permission = relationship('Permission')
3584
3590
3585 @classmethod
3591 @classmethod
3586 def create(cls, user, repository_group, permission):
3592 def create(cls, user, repository_group, permission):
3587 n = cls()
3593 n = cls()
3588 n.user = user
3594 n.user = user
3589 n.group = repository_group
3595 n.group = repository_group
3590 n.permission = permission
3596 n.permission = permission
3591 Session().add(n)
3597 Session().add(n)
3592 return n
3598 return n
3593
3599
3594
3600
3595 class UserGroupRepoGroupToPerm(Base, BaseModel):
3601 class UserGroupRepoGroupToPerm(Base, BaseModel):
3596 __tablename__ = 'users_group_repo_group_to_perm'
3602 __tablename__ = 'users_group_repo_group_to_perm'
3597 __table_args__ = (
3603 __table_args__ = (
3598 UniqueConstraint('users_group_id', 'group_id'),
3604 UniqueConstraint('users_group_id', 'group_id'),
3599 base_table_args
3605 base_table_args
3600 )
3606 )
3601
3607
3602 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)
3608 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)
3603 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3609 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3604 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3610 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3605 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3611 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3606
3612
3607 users_group = relationship('UserGroup', back_populates='users_group_repo_group_to_perm')
3613 users_group = relationship('UserGroup', back_populates='users_group_repo_group_to_perm')
3608 permission = relationship('Permission')
3614 permission = relationship('Permission')
3609 group = relationship('RepoGroup', back_populates='users_group_to_perm')
3615 group = relationship('RepoGroup', back_populates='users_group_to_perm')
3610
3616
3611 @classmethod
3617 @classmethod
3612 def create(cls, user_group, repository_group, permission):
3618 def create(cls, user_group, repository_group, permission):
3613 n = cls()
3619 n = cls()
3614 n.users_group = user_group
3620 n.users_group = user_group
3615 n.group = repository_group
3621 n.group = repository_group
3616 n.permission = permission
3622 n.permission = permission
3617 Session().add(n)
3623 Session().add(n)
3618 return n
3624 return n
3619
3625
3620 def __repr__(self):
3626 def __repr__(self):
3621 return '<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3627 return '<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3622
3628
3623
3629
3624 class Statistics(Base, BaseModel):
3630 class Statistics(Base, BaseModel):
3625 __tablename__ = 'statistics'
3631 __tablename__ = 'statistics'
3626 __table_args__ = (
3632 __table_args__ = (
3627 base_table_args
3633 base_table_args
3628 )
3634 )
3629
3635
3630 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3636 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3631 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3637 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3632 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3638 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3633 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False) #JSON data
3639 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False) #JSON data
3634 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False) #JSON data
3640 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False) #JSON data
3635 languages = Column("languages", LargeBinary(1000000), nullable=False) #JSON data
3641 languages = Column("languages", LargeBinary(1000000), nullable=False) #JSON data
3636
3642
3637 repository = relationship('Repository', single_parent=True, viewonly=True)
3643 repository = relationship('Repository', single_parent=True, viewonly=True)
3638
3644
3639
3645
3640 class UserFollowing(Base, BaseModel):
3646 class UserFollowing(Base, BaseModel):
3641 __tablename__ = 'user_followings'
3647 __tablename__ = 'user_followings'
3642 __table_args__ = (
3648 __table_args__ = (
3643 UniqueConstraint('user_id', 'follows_repository_id'),
3649 UniqueConstraint('user_id', 'follows_repository_id'),
3644 UniqueConstraint('user_id', 'follows_user_id'),
3650 UniqueConstraint('user_id', 'follows_user_id'),
3645 base_table_args
3651 base_table_args
3646 )
3652 )
3647
3653
3648 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3654 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3649 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3655 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3650 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3656 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3651 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3657 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3652 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3658 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3653
3659
3654 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id', back_populates='followings')
3660 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id', back_populates='followings')
3655
3661
3656 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3662 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3657 follows_repository = relationship('Repository', order_by='Repository.repo_name', back_populates='followers')
3663 follows_repository = relationship('Repository', order_by='Repository.repo_name', back_populates='followers')
3658
3664
3659 @classmethod
3665 @classmethod
3660 def get_repo_followers(cls, repo_id):
3666 def get_repo_followers(cls, repo_id):
3661 return cls.query().filter(cls.follows_repo_id == repo_id)
3667 return cls.query().filter(cls.follows_repo_id == repo_id)
3662
3668
3663
3669
3664 class CacheKey(Base, BaseModel):
3670 class CacheKey(Base, BaseModel):
3665 __tablename__ = 'cache_invalidation'
3671 __tablename__ = 'cache_invalidation'
3666 __table_args__ = (
3672 __table_args__ = (
3667 UniqueConstraint('cache_key'),
3673 UniqueConstraint('cache_key'),
3668 Index('key_idx', 'cache_key'),
3674 Index('key_idx', 'cache_key'),
3669 Index('cache_args_idx', 'cache_args'),
3675 Index('cache_args_idx', 'cache_args'),
3670 base_table_args,
3676 base_table_args,
3671 )
3677 )
3672
3678
3673 CACHE_TYPE_FEED = 'FEED'
3679 CACHE_TYPE_FEED = 'FEED'
3674
3680
3675 # namespaces used to register process/thread aware caches
3681 # namespaces used to register process/thread aware caches
3676 REPO_INVALIDATION_NAMESPACE = 'repo_cache.v1:{repo_id}'
3682 REPO_INVALIDATION_NAMESPACE = 'repo_cache.v1:{repo_id}'
3677
3683
3678 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3684 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3679 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3685 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3680 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3686 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3681 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3687 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3682 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3688 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3683
3689
3684 def __init__(self, cache_key, cache_args='', cache_state_uid=None, cache_active=False):
3690 def __init__(self, cache_key, cache_args='', cache_state_uid=None, cache_active=False):
3685 self.cache_key = cache_key
3691 self.cache_key = cache_key
3686 self.cache_args = cache_args
3692 self.cache_args = cache_args
3687 self.cache_active = cache_active
3693 self.cache_active = cache_active
3688 # first key should be same for all entries, since all workers should share it
3694 # first key should be same for all entries, since all workers should share it
3689 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3695 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3690
3696
3691 def __repr__(self):
3697 def __repr__(self):
3692 return "<%s('%s:%s[%s]')>" % (
3698 return "<%s('%s:%s[%s]')>" % (
3693 self.cls_name,
3699 self.cls_name,
3694 self.cache_id, self.cache_key, self.cache_active)
3700 self.cache_id, self.cache_key, self.cache_active)
3695
3701
3696 def _cache_key_partition(self):
3702 def _cache_key_partition(self):
3697 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3703 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3698 return prefix, repo_name, suffix
3704 return prefix, repo_name, suffix
3699
3705
3700 def get_prefix(self):
3706 def get_prefix(self):
3701 """
3707 """
3702 Try to extract prefix from existing cache key. The key could consist
3708 Try to extract prefix from existing cache key. The key could consist
3703 of prefix, repo_name, suffix
3709 of prefix, repo_name, suffix
3704 """
3710 """
3705 # this returns prefix, repo_name, suffix
3711 # this returns prefix, repo_name, suffix
3706 return self._cache_key_partition()[0]
3712 return self._cache_key_partition()[0]
3707
3713
3708 def get_suffix(self):
3714 def get_suffix(self):
3709 """
3715 """
3710 get suffix that might have been used in _get_cache_key to
3716 get suffix that might have been used in _get_cache_key to
3711 generate self.cache_key. Only used for informational purposes
3717 generate self.cache_key. Only used for informational purposes
3712 in repo_edit.mako.
3718 in repo_edit.mako.
3713 """
3719 """
3714 # prefix, repo_name, suffix
3720 # prefix, repo_name, suffix
3715 return self._cache_key_partition()[2]
3721 return self._cache_key_partition()[2]
3716
3722
3717 @classmethod
3723 @classmethod
3718 def generate_new_state_uid(cls, based_on=None):
3724 def generate_new_state_uid(cls, based_on=None):
3719 if based_on:
3725 if based_on:
3720 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3726 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3721 else:
3727 else:
3722 return str(uuid.uuid4())
3728 return str(uuid.uuid4())
3723
3729
3724 @classmethod
3730 @classmethod
3725 def delete_all_cache(cls):
3731 def delete_all_cache(cls):
3726 """
3732 """
3727 Delete all cache keys from database.
3733 Delete all cache keys from database.
3728 Should only be run when all instances are down and all entries
3734 Should only be run when all instances are down and all entries
3729 thus stale.
3735 thus stale.
3730 """
3736 """
3731 cls.query().delete()
3737 cls.query().delete()
3732 Session().commit()
3738 Session().commit()
3733
3739
3734 @classmethod
3740 @classmethod
3735 def set_invalidate(cls, cache_uid, delete=False):
3741 def set_invalidate(cls, cache_uid, delete=False):
3736 """
3742 """
3737 Mark all caches of a repo as invalid in the database.
3743 Mark all caches of a repo as invalid in the database.
3738 """
3744 """
3739 try:
3745 try:
3740 qry = Session().query(cls).filter(cls.cache_key == cache_uid)
3746 qry = Session().query(cls).filter(cls.cache_key == cache_uid)
3741 if delete:
3747 if delete:
3742 qry.delete()
3748 qry.delete()
3743 log.debug('cache objects deleted for cache args %s',
3749 log.debug('cache objects deleted for cache args %s',
3744 safe_str(cache_uid))
3750 safe_str(cache_uid))
3745 else:
3751 else:
3746 new_uid = cls.generate_new_state_uid()
3752 new_uid = cls.generate_new_state_uid()
3747 qry.update({"cache_state_uid": new_uid,
3753 qry.update({"cache_state_uid": new_uid,
3748 "cache_args": f"repo_state:{time.time()}"})
3754 "cache_args": f"repo_state:{time.time()}"})
3749 log.debug('cache object %s set new UID %s',
3755 log.debug('cache object %s set new UID %s',
3750 safe_str(cache_uid), new_uid)
3756 safe_str(cache_uid), new_uid)
3751
3757
3752 Session().commit()
3758 Session().commit()
3753 except Exception:
3759 except Exception:
3754 log.exception(
3760 log.exception(
3755 'Cache key invalidation failed for cache args %s',
3761 'Cache key invalidation failed for cache args %s',
3756 safe_str(cache_uid))
3762 safe_str(cache_uid))
3757 Session().rollback()
3763 Session().rollback()
3758
3764
3759 @classmethod
3765 @classmethod
3760 def get_active_cache(cls, cache_key):
3766 def get_active_cache(cls, cache_key):
3761 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3767 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3762 if inv_obj:
3768 if inv_obj:
3763 return inv_obj
3769 return inv_obj
3764 return None
3770 return None
3765
3771
3766 @classmethod
3772 @classmethod
3767 def get_namespace_map(cls, namespace):
3773 def get_namespace_map(cls, namespace):
3768 return {
3774 return {
3769 x.cache_key: x
3775 x.cache_key: x
3770 for x in cls.query().filter(cls.cache_args == namespace)}
3776 for x in cls.query().filter(cls.cache_args == namespace)}
3771
3777
3772
3778
3773 class ChangesetComment(Base, BaseModel):
3779 class ChangesetComment(Base, BaseModel):
3774 __tablename__ = 'changeset_comments'
3780 __tablename__ = 'changeset_comments'
3775 __table_args__ = (
3781 __table_args__ = (
3776 Index('cc_revision_idx', 'revision'),
3782 Index('cc_revision_idx', 'revision'),
3777 base_table_args,
3783 base_table_args,
3778 )
3784 )
3779
3785
3780 COMMENT_OUTDATED = 'comment_outdated'
3786 COMMENT_OUTDATED = 'comment_outdated'
3781 COMMENT_TYPE_NOTE = 'note'
3787 COMMENT_TYPE_NOTE = 'note'
3782 COMMENT_TYPE_TODO = 'todo'
3788 COMMENT_TYPE_TODO = 'todo'
3783 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3789 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3784
3790
3785 OP_IMMUTABLE = 'immutable'
3791 OP_IMMUTABLE = 'immutable'
3786 OP_CHANGEABLE = 'changeable'
3792 OP_CHANGEABLE = 'changeable'
3787
3793
3788 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3794 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3789 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3795 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3790 revision = Column('revision', String(40), nullable=True)
3796 revision = Column('revision', String(40), nullable=True)
3791 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3797 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3792 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3798 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3793 line_no = Column('line_no', Unicode(10), nullable=True)
3799 line_no = Column('line_no', Unicode(10), nullable=True)
3794 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3800 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3795 f_path = Column('f_path', Unicode(1000), nullable=True)
3801 f_path = Column('f_path', Unicode(1000), nullable=True)
3796 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3802 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3797 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3803 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3798 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3804 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3799 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3805 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3800 renderer = Column('renderer', Unicode(64), nullable=True)
3806 renderer = Column('renderer', Unicode(64), nullable=True)
3801 display_state = Column('display_state', Unicode(128), nullable=True)
3807 display_state = Column('display_state', Unicode(128), nullable=True)
3802 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3808 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3803 draft = Column('draft', Boolean(), nullable=True, default=False)
3809 draft = Column('draft', Boolean(), nullable=True, default=False)
3804
3810
3805 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3811 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3806 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3812 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3807
3813
3808 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3814 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3809 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3815 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3810
3816
3811 author = relationship('User', lazy='select', back_populates='user_comments')
3817 author = relationship('User', lazy='select', back_populates='user_comments')
3812 repo = relationship('Repository', back_populates='comments')
3818 repo = relationship('Repository', back_populates='comments')
3813 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='select', back_populates='comment')
3819 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='select', back_populates='comment')
3814 pull_request = relationship('PullRequest', lazy='select', back_populates='comments')
3820 pull_request = relationship('PullRequest', lazy='select', back_populates='comments')
3815 pull_request_version = relationship('PullRequestVersion', lazy='select')
3821 pull_request_version = relationship('PullRequestVersion', lazy='select')
3816 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='select', order_by='ChangesetCommentHistory.version', back_populates="comment")
3822 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='select', order_by='ChangesetCommentHistory.version', back_populates="comment")
3817
3823
3818 @classmethod
3824 @classmethod
3819 def get_users(cls, revision=None, pull_request_id=None):
3825 def get_users(cls, revision=None, pull_request_id=None):
3820 """
3826 """
3821 Returns user associated with this ChangesetComment. ie those
3827 Returns user associated with this ChangesetComment. ie those
3822 who actually commented
3828 who actually commented
3823
3829
3824 :param cls:
3830 :param cls:
3825 :param revision:
3831 :param revision:
3826 """
3832 """
3827 q = Session().query(User).join(ChangesetComment.author)
3833 q = Session().query(User).join(ChangesetComment.author)
3828 if revision:
3834 if revision:
3829 q = q.filter(cls.revision == revision)
3835 q = q.filter(cls.revision == revision)
3830 elif pull_request_id:
3836 elif pull_request_id:
3831 q = q.filter(cls.pull_request_id == pull_request_id)
3837 q = q.filter(cls.pull_request_id == pull_request_id)
3832 return q.all()
3838 return q.all()
3833
3839
3834 @classmethod
3840 @classmethod
3835 def get_index_from_version(cls, pr_version, versions=None, num_versions=None) -> int:
3841 def get_index_from_version(cls, pr_version, versions=None, num_versions=None) -> int:
3836 if pr_version is None:
3842 if pr_version is None:
3837 return 0
3843 return 0
3838
3844
3839 if versions is not None:
3845 if versions is not None:
3840 num_versions = [x.pull_request_version_id for x in versions]
3846 num_versions = [x.pull_request_version_id for x in versions]
3841
3847
3842 num_versions = num_versions or []
3848 num_versions = num_versions or []
3843 try:
3849 try:
3844 return num_versions.index(pr_version) + 1
3850 return num_versions.index(pr_version) + 1
3845 except (IndexError, ValueError):
3851 except (IndexError, ValueError):
3846 return 0
3852 return 0
3847
3853
3848 @property
3854 @property
3849 def outdated(self):
3855 def outdated(self):
3850 return self.display_state == self.COMMENT_OUTDATED
3856 return self.display_state == self.COMMENT_OUTDATED
3851
3857
3852 @property
3858 @property
3853 def outdated_js(self):
3859 def outdated_js(self):
3854 return str_json(self.display_state == self.COMMENT_OUTDATED)
3860 return str_json(self.display_state == self.COMMENT_OUTDATED)
3855
3861
3856 @property
3862 @property
3857 def immutable(self):
3863 def immutable(self):
3858 return self.immutable_state == self.OP_IMMUTABLE
3864 return self.immutable_state == self.OP_IMMUTABLE
3859
3865
3860 def outdated_at_version(self, version: int) -> bool:
3866 def outdated_at_version(self, version: int) -> bool:
3861 """
3867 """
3862 Checks if comment is outdated for given pull request version
3868 Checks if comment is outdated for given pull request version
3863 """
3869 """
3864
3870
3865 def version_check():
3871 def version_check():
3866 return self.pull_request_version_id and self.pull_request_version_id != version
3872 return self.pull_request_version_id and self.pull_request_version_id != version
3867
3873
3868 if self.is_inline:
3874 if self.is_inline:
3869 return self.outdated and version_check()
3875 return self.outdated and version_check()
3870 else:
3876 else:
3871 # general comments don't have .outdated set, also latest don't have a version
3877 # general comments don't have .outdated set, also latest don't have a version
3872 return version_check()
3878 return version_check()
3873
3879
3874 def outdated_at_version_js(self, version):
3880 def outdated_at_version_js(self, version):
3875 """
3881 """
3876 Checks if comment is outdated for given pull request version
3882 Checks if comment is outdated for given pull request version
3877 """
3883 """
3878 return str_json(self.outdated_at_version(version))
3884 return str_json(self.outdated_at_version(version))
3879
3885
3880 def older_than_version(self, version: int) -> bool:
3886 def older_than_version(self, version: int) -> bool:
3881 """
3887 """
3882 Checks if comment is made from a previous version than given.
3888 Checks if comment is made from a previous version than given.
3883 Assumes self.pull_request_version.pull_request_version_id is an integer if not None.
3889 Assumes self.pull_request_version.pull_request_version_id is an integer if not None.
3884 """
3890 """
3885
3891
3886 # If version is None, return False as the current version cannot be less than None
3892 # If version is None, return False as the current version cannot be less than None
3887 if version is None:
3893 if version is None:
3888 return False
3894 return False
3889
3895
3890 # Ensure that the version is an integer to prevent TypeError on comparison
3896 # Ensure that the version is an integer to prevent TypeError on comparison
3891 if not isinstance(version, int):
3897 if not isinstance(version, int):
3892 raise ValueError("The provided version must be an integer.")
3898 raise ValueError("The provided version must be an integer.")
3893
3899
3894 # Initialize current version to 0 or pull_request_version_id if it's available
3900 # Initialize current version to 0 or pull_request_version_id if it's available
3895 cur_ver = 0
3901 cur_ver = 0
3896 if self.pull_request_version and self.pull_request_version.pull_request_version_id is not None:
3902 if self.pull_request_version and self.pull_request_version.pull_request_version_id is not None:
3897 cur_ver = self.pull_request_version.pull_request_version_id
3903 cur_ver = self.pull_request_version.pull_request_version_id
3898
3904
3899 # Return True if the current version is less than the given version
3905 # Return True if the current version is less than the given version
3900 return cur_ver < version
3906 return cur_ver < version
3901
3907
3902 def older_than_version_js(self, version):
3908 def older_than_version_js(self, version):
3903 """
3909 """
3904 Checks if comment is made from previous version than given
3910 Checks if comment is made from previous version than given
3905 """
3911 """
3906 return str_json(self.older_than_version(version))
3912 return str_json(self.older_than_version(version))
3907
3913
3908 @property
3914 @property
3909 def commit_id(self):
3915 def commit_id(self):
3910 """New style naming to stop using .revision"""
3916 """New style naming to stop using .revision"""
3911 return self.revision
3917 return self.revision
3912
3918
3913 @property
3919 @property
3914 def resolved(self):
3920 def resolved(self):
3915 return self.resolved_by[0] if self.resolved_by else None
3921 return self.resolved_by[0] if self.resolved_by else None
3916
3922
3917 @property
3923 @property
3918 def is_todo(self):
3924 def is_todo(self):
3919 return self.comment_type == self.COMMENT_TYPE_TODO
3925 return self.comment_type == self.COMMENT_TYPE_TODO
3920
3926
3921 @property
3927 @property
3922 def is_inline(self):
3928 def is_inline(self):
3923 if self.line_no and self.f_path:
3929 if self.line_no and self.f_path:
3924 return True
3930 return True
3925 return False
3931 return False
3926
3932
3927 @property
3933 @property
3928 def last_version(self):
3934 def last_version(self):
3929 version = 0
3935 version = 0
3930 if self.history:
3936 if self.history:
3931 version = self.history[-1].version
3937 version = self.history[-1].version
3932 return version
3938 return version
3933
3939
3934 def get_index_version(self, versions):
3940 def get_index_version(self, versions):
3935 return self.get_index_from_version(
3941 return self.get_index_from_version(
3936 self.pull_request_version_id, versions)
3942 self.pull_request_version_id, versions)
3937
3943
3938 @property
3944 @property
3939 def review_status(self):
3945 def review_status(self):
3940 if self.status_change:
3946 if self.status_change:
3941 return self.status_change[0].status
3947 return self.status_change[0].status
3942
3948
3943 @property
3949 @property
3944 def review_status_lbl(self):
3950 def review_status_lbl(self):
3945 if self.status_change:
3951 if self.status_change:
3946 return self.status_change[0].status_lbl
3952 return self.status_change[0].status_lbl
3947
3953
3948 def __repr__(self):
3954 def __repr__(self):
3949 if self.comment_id:
3955 if self.comment_id:
3950 return f'<DB:Comment #{self.comment_id}>'
3956 return f'<DB:Comment #{self.comment_id}>'
3951 else:
3957 else:
3952 return f'<DB:Comment at {id(self)!r}>'
3958 return f'<DB:Comment at {id(self)!r}>'
3953
3959
3954 def get_api_data(self):
3960 def get_api_data(self):
3955 comment = self
3961 comment = self
3956
3962
3957 data = {
3963 data = {
3958 'comment_id': comment.comment_id,
3964 'comment_id': comment.comment_id,
3959 'comment_type': comment.comment_type,
3965 'comment_type': comment.comment_type,
3960 'comment_text': comment.text,
3966 'comment_text': comment.text,
3961 'comment_status': comment.status_change,
3967 'comment_status': comment.status_change,
3962 'comment_f_path': comment.f_path,
3968 'comment_f_path': comment.f_path,
3963 'comment_lineno': comment.line_no,
3969 'comment_lineno': comment.line_no,
3964 'comment_author': comment.author,
3970 'comment_author': comment.author,
3965 'comment_created_on': comment.created_on,
3971 'comment_created_on': comment.created_on,
3966 'comment_resolved_by': self.resolved,
3972 'comment_resolved_by': self.resolved,
3967 'comment_commit_id': comment.revision,
3973 'comment_commit_id': comment.revision,
3968 'comment_pull_request_id': comment.pull_request_id,
3974 'comment_pull_request_id': comment.pull_request_id,
3969 'comment_last_version': self.last_version
3975 'comment_last_version': self.last_version
3970 }
3976 }
3971 return data
3977 return data
3972
3978
3973 def __json__(self):
3979 def __json__(self):
3974 data = dict()
3980 data = dict()
3975 data.update(self.get_api_data())
3981 data.update(self.get_api_data())
3976 return data
3982 return data
3977
3983
3978
3984
3979 class ChangesetCommentHistory(Base, BaseModel):
3985 class ChangesetCommentHistory(Base, BaseModel):
3980 __tablename__ = 'changeset_comments_history'
3986 __tablename__ = 'changeset_comments_history'
3981 __table_args__ = (
3987 __table_args__ = (
3982 Index('cch_comment_id_idx', 'comment_id'),
3988 Index('cch_comment_id_idx', 'comment_id'),
3983 base_table_args,
3989 base_table_args,
3984 )
3990 )
3985
3991
3986 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
3992 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
3987 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
3993 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
3988 version = Column("version", Integer(), nullable=False, default=0)
3994 version = Column("version", Integer(), nullable=False, default=0)
3989 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3995 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3990 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3996 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3991 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3997 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3992 deleted = Column('deleted', Boolean(), default=False)
3998 deleted = Column('deleted', Boolean(), default=False)
3993
3999
3994 author = relationship('User', lazy='joined')
4000 author = relationship('User', lazy='joined')
3995 comment = relationship('ChangesetComment', cascade="all, delete", back_populates="history")
4001 comment = relationship('ChangesetComment', cascade="all, delete", back_populates="history")
3996
4002
3997 @classmethod
4003 @classmethod
3998 def get_version(cls, comment_id):
4004 def get_version(cls, comment_id):
3999 q = Session().query(ChangesetCommentHistory).filter(
4005 q = Session().query(ChangesetCommentHistory).filter(
4000 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
4006 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
4001 if q.count() == 0:
4007 if q.count() == 0:
4002 return 1
4008 return 1
4003 elif q.count() >= q[0].version:
4009 elif q.count() >= q[0].version:
4004 return q.count() + 1
4010 return q.count() + 1
4005 else:
4011 else:
4006 return q[0].version + 1
4012 return q[0].version + 1
4007
4013
4008
4014
4009 class ChangesetStatus(Base, BaseModel):
4015 class ChangesetStatus(Base, BaseModel):
4010 __tablename__ = 'changeset_statuses'
4016 __tablename__ = 'changeset_statuses'
4011 __table_args__ = (
4017 __table_args__ = (
4012 Index('cs_revision_idx', 'revision'),
4018 Index('cs_revision_idx', 'revision'),
4013 Index('cs_version_idx', 'version'),
4019 Index('cs_version_idx', 'version'),
4014 UniqueConstraint('repo_id', 'revision', 'version'),
4020 UniqueConstraint('repo_id', 'revision', 'version'),
4015 base_table_args
4021 base_table_args
4016 )
4022 )
4017
4023
4018 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
4024 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
4019 STATUS_APPROVED = 'approved'
4025 STATUS_APPROVED = 'approved'
4020 STATUS_REJECTED = 'rejected'
4026 STATUS_REJECTED = 'rejected'
4021 STATUS_UNDER_REVIEW = 'under_review'
4027 STATUS_UNDER_REVIEW = 'under_review'
4022
4028
4023 STATUSES = [
4029 STATUSES = [
4024 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
4030 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
4025 (STATUS_APPROVED, _("Approved")),
4031 (STATUS_APPROVED, _("Approved")),
4026 (STATUS_REJECTED, _("Rejected")),
4032 (STATUS_REJECTED, _("Rejected")),
4027 (STATUS_UNDER_REVIEW, _("Under Review")),
4033 (STATUS_UNDER_REVIEW, _("Under Review")),
4028 ]
4034 ]
4029
4035
4030 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
4036 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
4031 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
4037 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
4032 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
4038 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
4033 revision = Column('revision', String(40), nullable=False)
4039 revision = Column('revision', String(40), nullable=False)
4034 status = Column('status', String(128), nullable=False, default=DEFAULT)
4040 status = Column('status', String(128), nullable=False, default=DEFAULT)
4035 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
4041 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
4036 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
4042 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
4037 version = Column('version', Integer(), nullable=False, default=0)
4043 version = Column('version', Integer(), nullable=False, default=0)
4038 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
4044 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
4039
4045
4040 author = relationship('User', lazy='select')
4046 author = relationship('User', lazy='select')
4041 repo = relationship('Repository', lazy='select')
4047 repo = relationship('Repository', lazy='select')
4042 comment = relationship('ChangesetComment', lazy='select', back_populates='status_change')
4048 comment = relationship('ChangesetComment', lazy='select', back_populates='status_change')
4043 pull_request = relationship('PullRequest', lazy='select', back_populates='statuses')
4049 pull_request = relationship('PullRequest', lazy='select', back_populates='statuses')
4044
4050
4045 def __repr__(self):
4051 def __repr__(self):
4046 return f"<{self.cls_name}('{self.status}[v{self.version}]:{self.author}')>"
4052 return f"<{self.cls_name}('{self.status}[v{self.version}]:{self.author}')>"
4047
4053
4048 @classmethod
4054 @classmethod
4049 def get_status_lbl(cls, value):
4055 def get_status_lbl(cls, value):
4050 return dict(cls.STATUSES).get(value)
4056 return dict(cls.STATUSES).get(value)
4051
4057
4052 @property
4058 @property
4053 def status_lbl(self):
4059 def status_lbl(self):
4054 return ChangesetStatus.get_status_lbl(self.status)
4060 return ChangesetStatus.get_status_lbl(self.status)
4055
4061
4056 def get_api_data(self):
4062 def get_api_data(self):
4057 status = self
4063 status = self
4058 data = {
4064 data = {
4059 'status_id': status.changeset_status_id,
4065 'status_id': status.changeset_status_id,
4060 'status': status.status,
4066 'status': status.status,
4061 }
4067 }
4062 return data
4068 return data
4063
4069
4064 def __json__(self):
4070 def __json__(self):
4065 data = dict()
4071 data = dict()
4066 data.update(self.get_api_data())
4072 data.update(self.get_api_data())
4067 return data
4073 return data
4068
4074
4069
4075
4070 class _SetState(object):
4076 class _SetState(object):
4071 """
4077 """
4072 Context processor allowing changing state for sensitive operation such as
4078 Context processor allowing changing state for sensitive operation such as
4073 pull request update or merge
4079 pull request update or merge
4074 """
4080 """
4075
4081
4076 def __init__(self, pull_request, pr_state, back_state=None):
4082 def __init__(self, pull_request, pr_state, back_state=None):
4077 self._pr = pull_request
4083 self._pr = pull_request
4078 self._org_state = back_state or pull_request.pull_request_state
4084 self._org_state = back_state or pull_request.pull_request_state
4079 self._pr_state = pr_state
4085 self._pr_state = pr_state
4080 self._current_state = None
4086 self._current_state = None
4081
4087
4082 def __enter__(self):
4088 def __enter__(self):
4083 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4089 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4084 self._pr, self._pr_state)
4090 self._pr, self._pr_state)
4085 self.set_pr_state(self._pr_state)
4091 self.set_pr_state(self._pr_state)
4086 return self
4092 return self
4087
4093
4088 def __exit__(self, exc_type, exc_val, exc_tb):
4094 def __exit__(self, exc_type, exc_val, exc_tb):
4089 if exc_val is not None or exc_type is not None:
4095 if exc_val is not None or exc_type is not None:
4090 log.error(traceback.format_tb(exc_tb))
4096 log.error(traceback.format_tb(exc_tb))
4091 return None
4097 return None
4092
4098
4093 self.set_pr_state(self._org_state)
4099 self.set_pr_state(self._org_state)
4094 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4100 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4095 self._pr, self._org_state)
4101 self._pr, self._org_state)
4096
4102
4097 @property
4103 @property
4098 def state(self):
4104 def state(self):
4099 return self._current_state
4105 return self._current_state
4100
4106
4101 def set_pr_state(self, pr_state):
4107 def set_pr_state(self, pr_state):
4102 try:
4108 try:
4103 self._pr.pull_request_state = pr_state
4109 self._pr.pull_request_state = pr_state
4104 Session().add(self._pr)
4110 Session().add(self._pr)
4105 Session().commit()
4111 Session().commit()
4106 self._current_state = pr_state
4112 self._current_state = pr_state
4107 except Exception:
4113 except Exception:
4108 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4114 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4109 raise
4115 raise
4110
4116
4111
4117
4112 class _PullRequestBase(BaseModel):
4118 class _PullRequestBase(BaseModel):
4113 """
4119 """
4114 Common attributes of pull request and version entries.
4120 Common attributes of pull request and version entries.
4115 """
4121 """
4116
4122
4117 # .status values
4123 # .status values
4118 STATUS_NEW = 'new'
4124 STATUS_NEW = 'new'
4119 STATUS_OPEN = 'open'
4125 STATUS_OPEN = 'open'
4120 STATUS_CLOSED = 'closed'
4126 STATUS_CLOSED = 'closed'
4121
4127
4122 # available states
4128 # available states
4123 STATE_CREATING = 'creating'
4129 STATE_CREATING = 'creating'
4124 STATE_UPDATING = 'updating'
4130 STATE_UPDATING = 'updating'
4125 STATE_MERGING = 'merging'
4131 STATE_MERGING = 'merging'
4126 STATE_CREATED = 'created'
4132 STATE_CREATED = 'created'
4127
4133
4128 title = Column('title', Unicode(255), nullable=True)
4134 title = Column('title', Unicode(255), nullable=True)
4129 description = Column(
4135 description = Column(
4130 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4136 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4131 nullable=True)
4137 nullable=True)
4132 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4138 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4133
4139
4134 # new/open/closed status of pull request (not approve/reject/etc)
4140 # new/open/closed status of pull request (not approve/reject/etc)
4135 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4141 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4136 created_on = Column(
4142 created_on = Column(
4137 'created_on', DateTime(timezone=False), nullable=False,
4143 'created_on', DateTime(timezone=False), nullable=False,
4138 default=datetime.datetime.now)
4144 default=datetime.datetime.now)
4139 updated_on = Column(
4145 updated_on = Column(
4140 'updated_on', DateTime(timezone=False), nullable=False,
4146 'updated_on', DateTime(timezone=False), nullable=False,
4141 default=datetime.datetime.now)
4147 default=datetime.datetime.now)
4142
4148
4143 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4149 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4144
4150
4145 @declared_attr
4151 @declared_attr
4146 def user_id(cls):
4152 def user_id(cls):
4147 return Column(
4153 return Column(
4148 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4154 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4149 unique=None)
4155 unique=None)
4150
4156
4151 # 500 revisions max
4157 # 500 revisions max
4152 _revisions = Column(
4158 _revisions = Column(
4153 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4159 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4154
4160
4155 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4161 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4156
4162
4157 @declared_attr
4163 @declared_attr
4158 def source_repo_id(cls):
4164 def source_repo_id(cls):
4159 # TODO: dan: rename column to source_repo_id
4165 # TODO: dan: rename column to source_repo_id
4160 return Column(
4166 return Column(
4161 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4167 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4162 nullable=False)
4168 nullable=False)
4163
4169
4164 @declared_attr
4170 @declared_attr
4165 def pr_source(cls):
4171 def pr_source(cls):
4166 return relationship(
4172 return relationship(
4167 'Repository',
4173 'Repository',
4168 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4174 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4169 overlaps="pull_requests_source"
4175 overlaps="pull_requests_source"
4170 )
4176 )
4171
4177
4172 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4178 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4173
4179
4174 @hybrid_property
4180 @hybrid_property
4175 def source_ref(self):
4181 def source_ref(self):
4176 return self._source_ref
4182 return self._source_ref
4177
4183
4178 @source_ref.setter
4184 @source_ref.setter
4179 def source_ref(self, val):
4185 def source_ref(self, val):
4180 parts = (val or '').split(':')
4186 parts = (val or '').split(':')
4181 if len(parts) != 3:
4187 if len(parts) != 3:
4182 raise ValueError(
4188 raise ValueError(
4183 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4189 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4184 self._source_ref = safe_str(val)
4190 self._source_ref = safe_str(val)
4185
4191
4186 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4192 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4187
4193
4188 @hybrid_property
4194 @hybrid_property
4189 def target_ref(self):
4195 def target_ref(self):
4190 return self._target_ref
4196 return self._target_ref
4191
4197
4192 @target_ref.setter
4198 @target_ref.setter
4193 def target_ref(self, val):
4199 def target_ref(self, val):
4194 parts = (val or '').split(':')
4200 parts = (val or '').split(':')
4195 if len(parts) != 3:
4201 if len(parts) != 3:
4196 raise ValueError(
4202 raise ValueError(
4197 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4203 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4198 self._target_ref = safe_str(val)
4204 self._target_ref = safe_str(val)
4199
4205
4200 @declared_attr
4206 @declared_attr
4201 def target_repo_id(cls):
4207 def target_repo_id(cls):
4202 # TODO: dan: rename column to target_repo_id
4208 # TODO: dan: rename column to target_repo_id
4203 return Column(
4209 return Column(
4204 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4210 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4205 nullable=False)
4211 nullable=False)
4206
4212
4207 @declared_attr
4213 @declared_attr
4208 def pr_target(cls):
4214 def pr_target(cls):
4209 return relationship(
4215 return relationship(
4210 'Repository',
4216 'Repository',
4211 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4217 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4212 overlaps="pull_requests_target"
4218 overlaps="pull_requests_target"
4213 )
4219 )
4214
4220
4215 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4221 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4216
4222
4217 # TODO: dan: rename column to last_merge_source_rev
4223 # TODO: dan: rename column to last_merge_source_rev
4218 _last_merge_source_rev = Column(
4224 _last_merge_source_rev = Column(
4219 'last_merge_org_rev', String(40), nullable=True)
4225 'last_merge_org_rev', String(40), nullable=True)
4220 # TODO: dan: rename column to last_merge_target_rev
4226 # TODO: dan: rename column to last_merge_target_rev
4221 _last_merge_target_rev = Column(
4227 _last_merge_target_rev = Column(
4222 'last_merge_other_rev', String(40), nullable=True)
4228 'last_merge_other_rev', String(40), nullable=True)
4223 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4229 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4224 last_merge_metadata = Column(
4230 last_merge_metadata = Column(
4225 'last_merge_metadata', MutationObj.as_mutable(
4231 'last_merge_metadata', MutationObj.as_mutable(
4226 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4232 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4227
4233
4228 merge_rev = Column('merge_rev', String(40), nullable=True)
4234 merge_rev = Column('merge_rev', String(40), nullable=True)
4229
4235
4230 reviewer_data = Column(
4236 reviewer_data = Column(
4231 'reviewer_data_json', MutationObj.as_mutable(
4237 'reviewer_data_json', MutationObj.as_mutable(
4232 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4238 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4233
4239
4234 @property
4240 @property
4235 def reviewer_data_json(self):
4241 def reviewer_data_json(self):
4236 return str_json(self.reviewer_data)
4242 return str_json(self.reviewer_data)
4237
4243
4238 @property
4244 @property
4239 def last_merge_metadata_parsed(self):
4245 def last_merge_metadata_parsed(self):
4240 metadata = {}
4246 metadata = {}
4241 if not self.last_merge_metadata:
4247 if not self.last_merge_metadata:
4242 return metadata
4248 return metadata
4243
4249
4244 if hasattr(self.last_merge_metadata, 'de_coerce'):
4250 if hasattr(self.last_merge_metadata, 'de_coerce'):
4245 for k, v in self.last_merge_metadata.de_coerce().items():
4251 for k, v in self.last_merge_metadata.de_coerce().items():
4246 if k in ['target_ref', 'source_ref']:
4252 if k in ['target_ref', 'source_ref']:
4247 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4253 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4248 else:
4254 else:
4249 if hasattr(v, 'de_coerce'):
4255 if hasattr(v, 'de_coerce'):
4250 metadata[k] = v.de_coerce()
4256 metadata[k] = v.de_coerce()
4251 else:
4257 else:
4252 metadata[k] = v
4258 metadata[k] = v
4253 return metadata
4259 return metadata
4254
4260
4255 @property
4261 @property
4256 def work_in_progress(self):
4262 def work_in_progress(self):
4257 """checks if pull request is work in progress by checking the title"""
4263 """checks if pull request is work in progress by checking the title"""
4258 title = self.title.upper()
4264 title = self.title.upper()
4259 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4265 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4260 return True
4266 return True
4261 return False
4267 return False
4262
4268
4263 @property
4269 @property
4264 def title_safe(self):
4270 def title_safe(self):
4265 return self.title\
4271 return self.title\
4266 .replace('{', '{{')\
4272 .replace('{', '{{')\
4267 .replace('}', '}}')
4273 .replace('}', '}}')
4268
4274
4269 @hybrid_property
4275 @hybrid_property
4270 def description_safe(self):
4276 def description_safe(self):
4271 from rhodecode.lib import helpers as h
4277 from rhodecode.lib import helpers as h
4272 return h.escape(self.description)
4278 return h.escape(self.description)
4273
4279
4274 @hybrid_property
4280 @hybrid_property
4275 def revisions(self):
4281 def revisions(self):
4276 return self._revisions.split(':') if self._revisions else []
4282 return self._revisions.split(':') if self._revisions else []
4277
4283
4278 @revisions.setter
4284 @revisions.setter
4279 def revisions(self, val):
4285 def revisions(self, val):
4280 self._revisions = ':'.join(val)
4286 self._revisions = ':'.join(val)
4281
4287
4282 @hybrid_property
4288 @hybrid_property
4283 def last_merge_status(self):
4289 def last_merge_status(self):
4284 return safe_int(self._last_merge_status)
4290 return safe_int(self._last_merge_status)
4285
4291
4286 @last_merge_status.setter
4292 @last_merge_status.setter
4287 def last_merge_status(self, val):
4293 def last_merge_status(self, val):
4288 self._last_merge_status = val
4294 self._last_merge_status = val
4289
4295
4290 @declared_attr
4296 @declared_attr
4291 def author(cls):
4297 def author(cls):
4292 return relationship(
4298 return relationship(
4293 'User', lazy='joined',
4299 'User', lazy='joined',
4294 #TODO, problem that is somehow :?
4300 #TODO, problem that is somehow :?
4295 #back_populates='user_pull_requests'
4301 #back_populates='user_pull_requests'
4296 )
4302 )
4297
4303
4298 @declared_attr
4304 @declared_attr
4299 def source_repo(cls):
4305 def source_repo(cls):
4300 return relationship(
4306 return relationship(
4301 'Repository',
4307 'Repository',
4302 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4308 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4303 overlaps="pr_source"
4309 overlaps="pr_source"
4304 )
4310 )
4305
4311
4306 @property
4312 @property
4307 def source_ref_parts(self):
4313 def source_ref_parts(self):
4308 return self.unicode_to_reference(self.source_ref)
4314 return self.unicode_to_reference(self.source_ref)
4309
4315
4310 @declared_attr
4316 @declared_attr
4311 def target_repo(cls):
4317 def target_repo(cls):
4312 return relationship(
4318 return relationship(
4313 'Repository',
4319 'Repository',
4314 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4320 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4315 overlaps="pr_target"
4321 overlaps="pr_target"
4316 )
4322 )
4317
4323
4318 @property
4324 @property
4319 def target_ref_parts(self):
4325 def target_ref_parts(self):
4320 return self.unicode_to_reference(self.target_ref)
4326 return self.unicode_to_reference(self.target_ref)
4321
4327
4322 @property
4328 @property
4323 def shadow_merge_ref(self):
4329 def shadow_merge_ref(self):
4324 return self.unicode_to_reference(self._shadow_merge_ref)
4330 return self.unicode_to_reference(self._shadow_merge_ref)
4325
4331
4326 @shadow_merge_ref.setter
4332 @shadow_merge_ref.setter
4327 def shadow_merge_ref(self, ref):
4333 def shadow_merge_ref(self, ref):
4328 self._shadow_merge_ref = self.reference_to_unicode(ref)
4334 self._shadow_merge_ref = self.reference_to_unicode(ref)
4329
4335
4330 @staticmethod
4336 @staticmethod
4331 def unicode_to_reference(raw):
4337 def unicode_to_reference(raw):
4332 return unicode_to_reference(raw)
4338 return unicode_to_reference(raw)
4333
4339
4334 @staticmethod
4340 @staticmethod
4335 def reference_to_unicode(ref):
4341 def reference_to_unicode(ref):
4336 return reference_to_unicode(ref)
4342 return reference_to_unicode(ref)
4337
4343
4338 def get_api_data(self, with_merge_state=True):
4344 def get_api_data(self, with_merge_state=True):
4339 from rhodecode.model.pull_request import PullRequestModel
4345 from rhodecode.model.pull_request import PullRequestModel
4340
4346
4341 pull_request = self
4347 pull_request = self
4342 if with_merge_state:
4348 if with_merge_state:
4343 merge_response, merge_status, msg = \
4349 merge_response, merge_status, msg = \
4344 PullRequestModel().merge_status(pull_request)
4350 PullRequestModel().merge_status(pull_request)
4345 merge_state = {
4351 merge_state = {
4346 'status': merge_status,
4352 'status': merge_status,
4347 'message': safe_str(msg),
4353 'message': safe_str(msg),
4348 }
4354 }
4349 else:
4355 else:
4350 merge_state = {'status': 'not_available',
4356 merge_state = {'status': 'not_available',
4351 'message': 'not_available'}
4357 'message': 'not_available'}
4352
4358
4353 merge_data = {
4359 merge_data = {
4354 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4360 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4355 'reference': (
4361 'reference': (
4356 pull_request.shadow_merge_ref.asdict()
4362 pull_request.shadow_merge_ref.asdict()
4357 if pull_request.shadow_merge_ref else None),
4363 if pull_request.shadow_merge_ref else None),
4358 }
4364 }
4359
4365
4360 data = {
4366 data = {
4361 'pull_request_id': pull_request.pull_request_id,
4367 'pull_request_id': pull_request.pull_request_id,
4362 'url': PullRequestModel().get_url(pull_request),
4368 'url': PullRequestModel().get_url(pull_request),
4363 'title': pull_request.title,
4369 'title': pull_request.title,
4364 'description': pull_request.description,
4370 'description': pull_request.description,
4365 'status': pull_request.status,
4371 'status': pull_request.status,
4366 'state': pull_request.pull_request_state,
4372 'state': pull_request.pull_request_state,
4367 'created_on': pull_request.created_on,
4373 'created_on': pull_request.created_on,
4368 'updated_on': pull_request.updated_on,
4374 'updated_on': pull_request.updated_on,
4369 'commit_ids': pull_request.revisions,
4375 'commit_ids': pull_request.revisions,
4370 'review_status': pull_request.calculated_review_status(),
4376 'review_status': pull_request.calculated_review_status(),
4371 'mergeable': merge_state,
4377 'mergeable': merge_state,
4372 'source': {
4378 'source': {
4373 'clone_url': pull_request.source_repo.clone_url(),
4379 'clone_url': pull_request.source_repo.clone_url(),
4374 'repository': pull_request.source_repo.repo_name,
4380 'repository': pull_request.source_repo.repo_name,
4375 'reference': {
4381 'reference': {
4376 'name': pull_request.source_ref_parts.name,
4382 'name': pull_request.source_ref_parts.name,
4377 'type': pull_request.source_ref_parts.type,
4383 'type': pull_request.source_ref_parts.type,
4378 'commit_id': pull_request.source_ref_parts.commit_id,
4384 'commit_id': pull_request.source_ref_parts.commit_id,
4379 },
4385 },
4380 },
4386 },
4381 'target': {
4387 'target': {
4382 'clone_url': pull_request.target_repo.clone_url(),
4388 'clone_url': pull_request.target_repo.clone_url(),
4383 'repository': pull_request.target_repo.repo_name,
4389 'repository': pull_request.target_repo.repo_name,
4384 'reference': {
4390 'reference': {
4385 'name': pull_request.target_ref_parts.name,
4391 'name': pull_request.target_ref_parts.name,
4386 'type': pull_request.target_ref_parts.type,
4392 'type': pull_request.target_ref_parts.type,
4387 'commit_id': pull_request.target_ref_parts.commit_id,
4393 'commit_id': pull_request.target_ref_parts.commit_id,
4388 },
4394 },
4389 },
4395 },
4390 'merge': merge_data,
4396 'merge': merge_data,
4391 'author': pull_request.author.get_api_data(include_secrets=False,
4397 'author': pull_request.author.get_api_data(include_secrets=False,
4392 details='basic'),
4398 details='basic'),
4393 'reviewers': [
4399 'reviewers': [
4394 {
4400 {
4395 'user': reviewer.get_api_data(include_secrets=False,
4401 'user': reviewer.get_api_data(include_secrets=False,
4396 details='basic'),
4402 details='basic'),
4397 'reasons': reasons,
4403 'reasons': reasons,
4398 'review_status': st[0][1].status if st else 'not_reviewed',
4404 'review_status': st[0][1].status if st else 'not_reviewed',
4399 }
4405 }
4400 for obj, reviewer, reasons, mandatory, st in
4406 for obj, reviewer, reasons, mandatory, st in
4401 pull_request.reviewers_statuses()
4407 pull_request.reviewers_statuses()
4402 ]
4408 ]
4403 }
4409 }
4404
4410
4405 return data
4411 return data
4406
4412
4407 def set_state(self, pull_request_state, final_state=None):
4413 def set_state(self, pull_request_state, final_state=None):
4408 """
4414 """
4409 # goes from initial state to updating to initial state.
4415 # goes from initial state to updating to initial state.
4410 # initial state can be changed by specifying back_state=
4416 # initial state can be changed by specifying back_state=
4411 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4417 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4412 pull_request.merge()
4418 pull_request.merge()
4413
4419
4414 :param pull_request_state:
4420 :param pull_request_state:
4415 :param final_state:
4421 :param final_state:
4416
4422
4417 """
4423 """
4418
4424
4419 return _SetState(self, pull_request_state, back_state=final_state)
4425 return _SetState(self, pull_request_state, back_state=final_state)
4420
4426
4421
4427
4422 class PullRequest(Base, _PullRequestBase):
4428 class PullRequest(Base, _PullRequestBase):
4423 __tablename__ = 'pull_requests'
4429 __tablename__ = 'pull_requests'
4424 __table_args__ = (
4430 __table_args__ = (
4425 base_table_args,
4431 base_table_args,
4426 )
4432 )
4427 LATEST_VER = 'latest'
4433 LATEST_VER = 'latest'
4428
4434
4429 pull_request_id = Column(
4435 pull_request_id = Column(
4430 'pull_request_id', Integer(), nullable=False, primary_key=True)
4436 'pull_request_id', Integer(), nullable=False, primary_key=True)
4431
4437
4432 def __repr__(self):
4438 def __repr__(self):
4433 if self.pull_request_id:
4439 if self.pull_request_id:
4434 return f'<DB:PullRequest #{self.pull_request_id}>'
4440 return f'<DB:PullRequest #{self.pull_request_id}>'
4435 else:
4441 else:
4436 return f'<DB:PullRequest at {id(self)!r}>'
4442 return f'<DB:PullRequest at {id(self)!r}>'
4437
4443
4438 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan", back_populates='pull_request')
4444 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan", back_populates='pull_request')
4439 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan", back_populates='pull_request')
4445 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan", back_populates='pull_request')
4440 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='pull_request')
4446 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='pull_request')
4441 versions = relationship('PullRequestVersion', cascade="all, delete-orphan", lazy='dynamic', back_populates='pull_request')
4447 versions = relationship('PullRequestVersion', cascade="all, delete-orphan", lazy='dynamic', back_populates='pull_request')
4442
4448
4443 @classmethod
4449 @classmethod
4444 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4450 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4445 internal_methods=None):
4451 internal_methods=None):
4446
4452
4447 class PullRequestDisplay(object):
4453 class PullRequestDisplay(object):
4448 """
4454 """
4449 Special object wrapper for showing PullRequest data via Versions
4455 Special object wrapper for showing PullRequest data via Versions
4450 It mimics PR object as close as possible. This is read only object
4456 It mimics PR object as close as possible. This is read only object
4451 just for display
4457 just for display
4452 """
4458 """
4453
4459
4454 def __init__(self, attrs, internal=None):
4460 def __init__(self, attrs, internal=None):
4455 self.attrs = attrs
4461 self.attrs = attrs
4456 # internal have priority over the given ones via attrs
4462 # internal have priority over the given ones via attrs
4457 self.internal = internal or ['versions']
4463 self.internal = internal or ['versions']
4458
4464
4459 def __getattr__(self, item):
4465 def __getattr__(self, item):
4460 if item in self.internal:
4466 if item in self.internal:
4461 return getattr(self, item)
4467 return getattr(self, item)
4462 try:
4468 try:
4463 return self.attrs[item]
4469 return self.attrs[item]
4464 except KeyError:
4470 except KeyError:
4465 raise AttributeError(
4471 raise AttributeError(
4466 '%s object has no attribute %s' % (self, item))
4472 '%s object has no attribute %s' % (self, item))
4467
4473
4468 def __repr__(self):
4474 def __repr__(self):
4469 pr_id = self.attrs.get('pull_request_id')
4475 pr_id = self.attrs.get('pull_request_id')
4470 return f'<DB:PullRequestDisplay #{pr_id}>'
4476 return f'<DB:PullRequestDisplay #{pr_id}>'
4471
4477
4472 def versions(self):
4478 def versions(self):
4473 return pull_request_obj.versions.order_by(
4479 return pull_request_obj.versions.order_by(
4474 PullRequestVersion.pull_request_version_id).all()
4480 PullRequestVersion.pull_request_version_id).all()
4475
4481
4476 def is_closed(self):
4482 def is_closed(self):
4477 return pull_request_obj.is_closed()
4483 return pull_request_obj.is_closed()
4478
4484
4479 def is_state_changing(self):
4485 def is_state_changing(self):
4480 return pull_request_obj.is_state_changing()
4486 return pull_request_obj.is_state_changing()
4481
4487
4482 @property
4488 @property
4483 def pull_request_version_id(self):
4489 def pull_request_version_id(self):
4484 return getattr(pull_request_obj, 'pull_request_version_id', None)
4490 return getattr(pull_request_obj, 'pull_request_version_id', None)
4485
4491
4486 @property
4492 @property
4487 def pull_request_last_version(self):
4493 def pull_request_last_version(self):
4488 return pull_request_obj.pull_request_last_version
4494 return pull_request_obj.pull_request_last_version
4489
4495
4490 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4496 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4491
4497
4492 attrs.author = StrictAttributeDict(
4498 attrs.author = StrictAttributeDict(
4493 pull_request_obj.author.get_api_data())
4499 pull_request_obj.author.get_api_data())
4494 if pull_request_obj.target_repo:
4500 if pull_request_obj.target_repo:
4495 attrs.target_repo = StrictAttributeDict(
4501 attrs.target_repo = StrictAttributeDict(
4496 pull_request_obj.target_repo.get_api_data())
4502 pull_request_obj.target_repo.get_api_data())
4497 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4503 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4498
4504
4499 if pull_request_obj.source_repo:
4505 if pull_request_obj.source_repo:
4500 attrs.source_repo = StrictAttributeDict(
4506 attrs.source_repo = StrictAttributeDict(
4501 pull_request_obj.source_repo.get_api_data())
4507 pull_request_obj.source_repo.get_api_data())
4502 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4508 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4503
4509
4504 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4510 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4505 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4511 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4506 attrs.revisions = pull_request_obj.revisions
4512 attrs.revisions = pull_request_obj.revisions
4507 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4513 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4508 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4514 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4509 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4515 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4510 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4516 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4511
4517
4512 return PullRequestDisplay(attrs, internal=internal_methods)
4518 return PullRequestDisplay(attrs, internal=internal_methods)
4513
4519
4514 def is_closed(self):
4520 def is_closed(self):
4515 return self.status == self.STATUS_CLOSED
4521 return self.status == self.STATUS_CLOSED
4516
4522
4517 def is_state_changing(self):
4523 def is_state_changing(self):
4518 return self.pull_request_state != PullRequest.STATE_CREATED
4524 return self.pull_request_state != PullRequest.STATE_CREATED
4519
4525
4520 def __json__(self):
4526 def __json__(self):
4521 return {
4527 return {
4522 'revisions': self.revisions,
4528 'revisions': self.revisions,
4523 'versions': self.versions_count
4529 'versions': self.versions_count
4524 }
4530 }
4525
4531
4526 def calculated_review_status(self):
4532 def calculated_review_status(self):
4527 from rhodecode.model.changeset_status import ChangesetStatusModel
4533 from rhodecode.model.changeset_status import ChangesetStatusModel
4528 return ChangesetStatusModel().calculated_review_status(self)
4534 return ChangesetStatusModel().calculated_review_status(self)
4529
4535
4530 def reviewers_statuses(self, user=None):
4536 def reviewers_statuses(self, user=None):
4531 from rhodecode.model.changeset_status import ChangesetStatusModel
4537 from rhodecode.model.changeset_status import ChangesetStatusModel
4532 return ChangesetStatusModel().reviewers_statuses(self, user=user)
4538 return ChangesetStatusModel().reviewers_statuses(self, user=user)
4533
4539
4534 def get_pull_request_reviewers(self, role=None):
4540 def get_pull_request_reviewers(self, role=None):
4535 qry = PullRequestReviewers.query()\
4541 qry = PullRequestReviewers.query()\
4536 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4542 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4537 if role:
4543 if role:
4538 qry = qry.filter(PullRequestReviewers.role == role)
4544 qry = qry.filter(PullRequestReviewers.role == role)
4539
4545
4540 return qry.all()
4546 return qry.all()
4541
4547
4542 @property
4548 @property
4543 def reviewers_count(self):
4549 def reviewers_count(self):
4544 qry = PullRequestReviewers.query()\
4550 qry = PullRequestReviewers.query()\
4545 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4551 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4546 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4552 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4547 return qry.count()
4553 return qry.count()
4548
4554
4549 @property
4555 @property
4550 def observers_count(self):
4556 def observers_count(self):
4551 qry = PullRequestReviewers.query()\
4557 qry = PullRequestReviewers.query()\
4552 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4558 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4553 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4559 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4554 return qry.count()
4560 return qry.count()
4555
4561
4556 def observers(self):
4562 def observers(self):
4557 qry = PullRequestReviewers.query()\
4563 qry = PullRequestReviewers.query()\
4558 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4564 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4559 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4565 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4560 .all()
4566 .all()
4561
4567
4562 for entry in qry:
4568 for entry in qry:
4563 yield entry, entry.user
4569 yield entry, entry.user
4564
4570
4565 @property
4571 @property
4566 def workspace_id(self):
4572 def workspace_id(self):
4567 from rhodecode.model.pull_request import PullRequestModel
4573 from rhodecode.model.pull_request import PullRequestModel
4568 return PullRequestModel()._workspace_id(self)
4574 return PullRequestModel()._workspace_id(self)
4569
4575
4570 def get_shadow_repo(self):
4576 def get_shadow_repo(self):
4571 workspace_id = self.workspace_id
4577 workspace_id = self.workspace_id
4572 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4578 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4573 if os.path.isdir(shadow_repository_path):
4579 if os.path.isdir(shadow_repository_path):
4574 vcs_obj = self.target_repo.scm_instance()
4580 vcs_obj = self.target_repo.scm_instance()
4575 return vcs_obj.get_shadow_instance(shadow_repository_path)
4581 return vcs_obj.get_shadow_instance(shadow_repository_path)
4576
4582
4577 @property
4583 @property
4578 def versions_count(self):
4584 def versions_count(self):
4579 """
4585 """
4580 return number of versions this PR have, e.g a PR that once been
4586 return number of versions this PR have, e.g a PR that once been
4581 updated will have 2 versions
4587 updated will have 2 versions
4582 """
4588 """
4583 return self.versions.count() + 1
4589 return self.versions.count() + 1
4584
4590
4585 @property
4591 @property
4586 def pull_request_last_version(self):
4592 def pull_request_last_version(self):
4587 return self.versions_count
4593 return self.versions_count
4588
4594
4589
4595
4590 class PullRequestVersion(Base, _PullRequestBase):
4596 class PullRequestVersion(Base, _PullRequestBase):
4591 __tablename__ = 'pull_request_versions'
4597 __tablename__ = 'pull_request_versions'
4592 __table_args__ = (
4598 __table_args__ = (
4593 base_table_args,
4599 base_table_args,
4594 )
4600 )
4595
4601
4596 pull_request_version_id = Column('pull_request_version_id', Integer(), nullable=False, primary_key=True)
4602 pull_request_version_id = Column('pull_request_version_id', Integer(), nullable=False, primary_key=True)
4597 pull_request_id = Column('pull_request_id', Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
4603 pull_request_id = Column('pull_request_id', Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
4598 pull_request = relationship('PullRequest', back_populates='versions')
4604 pull_request = relationship('PullRequest', back_populates='versions')
4599
4605
4600 def __repr__(self):
4606 def __repr__(self):
4601 if self.pull_request_version_id:
4607 if self.pull_request_version_id:
4602 return f'<DB:PullRequestVersion #{self.pull_request_version_id}>'
4608 return f'<DB:PullRequestVersion #{self.pull_request_version_id}>'
4603 else:
4609 else:
4604 return f'<DB:PullRequestVersion at {id(self)!r}>'
4610 return f'<DB:PullRequestVersion at {id(self)!r}>'
4605
4611
4606 @property
4612 @property
4607 def reviewers(self):
4613 def reviewers(self):
4608 return self.pull_request.reviewers
4614 return self.pull_request.reviewers
4609
4615
4610 @property
4616 @property
4611 def versions(self):
4617 def versions(self):
4612 return self.pull_request.versions
4618 return self.pull_request.versions
4613
4619
4614 def is_closed(self):
4620 def is_closed(self):
4615 # calculate from original
4621 # calculate from original
4616 return self.pull_request.status == self.STATUS_CLOSED
4622 return self.pull_request.status == self.STATUS_CLOSED
4617
4623
4618 def is_state_changing(self):
4624 def is_state_changing(self):
4619 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4625 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4620
4626
4621 def calculated_review_status(self):
4627 def calculated_review_status(self):
4622 return self.pull_request.calculated_review_status()
4628 return self.pull_request.calculated_review_status()
4623
4629
4624 def reviewers_statuses(self):
4630 def reviewers_statuses(self):
4625 return self.pull_request.reviewers_statuses()
4631 return self.pull_request.reviewers_statuses()
4626
4632
4627 def observers(self):
4633 def observers(self):
4628 return self.pull_request.observers()
4634 return self.pull_request.observers()
4629
4635
4630
4636
4631 class PullRequestReviewers(Base, BaseModel):
4637 class PullRequestReviewers(Base, BaseModel):
4632 __tablename__ = 'pull_request_reviewers'
4638 __tablename__ = 'pull_request_reviewers'
4633 __table_args__ = (
4639 __table_args__ = (
4634 base_table_args,
4640 base_table_args,
4635 )
4641 )
4636 ROLE_REVIEWER = 'reviewer'
4642 ROLE_REVIEWER = 'reviewer'
4637 ROLE_OBSERVER = 'observer'
4643 ROLE_OBSERVER = 'observer'
4638 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4644 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4639
4645
4640 @hybrid_property
4646 @hybrid_property
4641 def reasons(self):
4647 def reasons(self):
4642 if not self._reasons:
4648 if not self._reasons:
4643 return []
4649 return []
4644 return self._reasons
4650 return self._reasons
4645
4651
4646 @reasons.setter
4652 @reasons.setter
4647 def reasons(self, val):
4653 def reasons(self, val):
4648 val = val or []
4654 val = val or []
4649 if any(not isinstance(x, str) for x in val):
4655 if any(not isinstance(x, str) for x in val):
4650 raise Exception('invalid reasons type, must be list of strings')
4656 raise Exception('invalid reasons type, must be list of strings')
4651 self._reasons = val
4657 self._reasons = val
4652
4658
4653 pull_requests_reviewers_id = Column(
4659 pull_requests_reviewers_id = Column(
4654 'pull_requests_reviewers_id', Integer(), nullable=False,
4660 'pull_requests_reviewers_id', Integer(), nullable=False,
4655 primary_key=True)
4661 primary_key=True)
4656 pull_request_id = Column(
4662 pull_request_id = Column(
4657 "pull_request_id", Integer(),
4663 "pull_request_id", Integer(),
4658 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4664 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4659 user_id = Column(
4665 user_id = Column(
4660 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4666 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4661 _reasons = Column(
4667 _reasons = Column(
4662 'reason', MutationList.as_mutable(
4668 'reason', MutationList.as_mutable(
4663 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4669 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4664
4670
4665 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4671 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4666 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4672 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4667
4673
4668 user = relationship('User')
4674 user = relationship('User')
4669 pull_request = relationship('PullRequest', back_populates='reviewers')
4675 pull_request = relationship('PullRequest', back_populates='reviewers')
4670
4676
4671 rule_data = Column(
4677 rule_data = Column(
4672 'rule_data_json',
4678 'rule_data_json',
4673 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4679 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4674
4680
4675 def rule_user_group_data(self):
4681 def rule_user_group_data(self):
4676 """
4682 """
4677 Returns the voting user group rule data for this reviewer
4683 Returns the voting user group rule data for this reviewer
4678 """
4684 """
4679
4685
4680 if self.rule_data and 'vote_rule' in self.rule_data:
4686 if self.rule_data and 'vote_rule' in self.rule_data:
4681 user_group_data = {}
4687 user_group_data = {}
4682 if 'rule_user_group_entry_id' in self.rule_data:
4688 if 'rule_user_group_entry_id' in self.rule_data:
4683 # means a group with voting rules !
4689 # means a group with voting rules !
4684 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4690 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4685 user_group_data['name'] = self.rule_data['rule_name']
4691 user_group_data['name'] = self.rule_data['rule_name']
4686 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4692 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4687
4693
4688 return user_group_data
4694 return user_group_data
4689
4695
4690 @classmethod
4696 @classmethod
4691 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4697 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4692 qry = PullRequestReviewers.query()\
4698 qry = PullRequestReviewers.query()\
4693 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4699 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4694 if role:
4700 if role:
4695 qry = qry.filter(PullRequestReviewers.role == role)
4701 qry = qry.filter(PullRequestReviewers.role == role)
4696
4702
4697 return qry.all()
4703 return qry.all()
4698
4704
4699 def __repr__(self):
4705 def __repr__(self):
4700 return f"<{self.cls_name}('id:{self.pull_requests_reviewers_id}')>"
4706 return f"<{self.cls_name}('id:{self.pull_requests_reviewers_id}')>"
4701
4707
4702
4708
4703 class Notification(Base, BaseModel):
4709 class Notification(Base, BaseModel):
4704 __tablename__ = 'notifications'
4710 __tablename__ = 'notifications'
4705 __table_args__ = (
4711 __table_args__ = (
4706 Index('notification_type_idx', 'type'),
4712 Index('notification_type_idx', 'type'),
4707 base_table_args,
4713 base_table_args,
4708 )
4714 )
4709
4715
4710 TYPE_CHANGESET_COMMENT = 'cs_comment'
4716 TYPE_CHANGESET_COMMENT = 'cs_comment'
4711 TYPE_MESSAGE = 'message'
4717 TYPE_MESSAGE = 'message'
4712 TYPE_MENTION = 'mention'
4718 TYPE_MENTION = 'mention'
4713 TYPE_REGISTRATION = 'registration'
4719 TYPE_REGISTRATION = 'registration'
4714 TYPE_PULL_REQUEST = 'pull_request'
4720 TYPE_PULL_REQUEST = 'pull_request'
4715 TYPE_PULL_REQUEST_COMMENT = 'pull_request_comment'
4721 TYPE_PULL_REQUEST_COMMENT = 'pull_request_comment'
4716 TYPE_PULL_REQUEST_UPDATE = 'pull_request_update'
4722 TYPE_PULL_REQUEST_UPDATE = 'pull_request_update'
4717
4723
4718 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4724 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4719 subject = Column('subject', Unicode(512), nullable=True)
4725 subject = Column('subject', Unicode(512), nullable=True)
4720 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4726 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4721 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4727 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4722 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4728 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4723 type_ = Column('type', Unicode(255))
4729 type_ = Column('type', Unicode(255))
4724
4730
4725 created_by_user = relationship('User', back_populates='user_created_notifications')
4731 created_by_user = relationship('User', back_populates='user_created_notifications')
4726 notifications_to_users = relationship('UserNotification', lazy='joined', cascade="all, delete-orphan", back_populates='notification')
4732 notifications_to_users = relationship('UserNotification', lazy='joined', cascade="all, delete-orphan", back_populates='notification')
4727
4733
4728 @property
4734 @property
4729 def recipients(self):
4735 def recipients(self):
4730 return [x.user for x in UserNotification.query()\
4736 return [x.user for x in UserNotification.query()\
4731 .filter(UserNotification.notification == self)\
4737 .filter(UserNotification.notification == self)\
4732 .order_by(UserNotification.user_id.asc()).all()]
4738 .order_by(UserNotification.user_id.asc()).all()]
4733
4739
4734 @classmethod
4740 @classmethod
4735 def create(cls, created_by, subject, body, recipients, type_=None):
4741 def create(cls, created_by, subject, body, recipients, type_=None):
4736 if type_ is None:
4742 if type_ is None:
4737 type_ = Notification.TYPE_MESSAGE
4743 type_ = Notification.TYPE_MESSAGE
4738
4744
4739 notification = cls()
4745 notification = cls()
4740 notification.created_by_user = created_by
4746 notification.created_by_user = created_by
4741 notification.subject = subject
4747 notification.subject = subject
4742 notification.body = body
4748 notification.body = body
4743 notification.type_ = type_
4749 notification.type_ = type_
4744 notification.created_on = datetime.datetime.now()
4750 notification.created_on = datetime.datetime.now()
4745
4751
4746 # For each recipient link the created notification to his account
4752 # For each recipient link the created notification to his account
4747 for u in recipients:
4753 for u in recipients:
4748 assoc = UserNotification()
4754 assoc = UserNotification()
4749 assoc.user_id = u.user_id
4755 assoc.user_id = u.user_id
4750 assoc.notification = notification
4756 assoc.notification = notification
4751
4757
4752 # if created_by is inside recipients mark his notification
4758 # if created_by is inside recipients mark his notification
4753 # as read
4759 # as read
4754 if u.user_id == created_by.user_id:
4760 if u.user_id == created_by.user_id:
4755 assoc.read = True
4761 assoc.read = True
4756 Session().add(assoc)
4762 Session().add(assoc)
4757
4763
4758 Session().add(notification)
4764 Session().add(notification)
4759
4765
4760 return notification
4766 return notification
4761
4767
4762
4768
4763 class UserNotification(Base, BaseModel):
4769 class UserNotification(Base, BaseModel):
4764 __tablename__ = 'user_to_notification'
4770 __tablename__ = 'user_to_notification'
4765 __table_args__ = (
4771 __table_args__ = (
4766 UniqueConstraint('user_id', 'notification_id'),
4772 UniqueConstraint('user_id', 'notification_id'),
4767 base_table_args
4773 base_table_args
4768 )
4774 )
4769
4775
4770 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4776 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4771 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4777 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4772 read = Column('read', Boolean, default=False)
4778 read = Column('read', Boolean, default=False)
4773 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4779 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4774
4780
4775 user = relationship('User', lazy="joined", back_populates='notifications')
4781 user = relationship('User', lazy="joined", back_populates='notifications')
4776 notification = relationship('Notification', lazy="joined", order_by=lambda: Notification.created_on.desc(), back_populates='notifications_to_users')
4782 notification = relationship('Notification', lazy="joined", order_by=lambda: Notification.created_on.desc(), back_populates='notifications_to_users')
4777
4783
4778 def mark_as_read(self):
4784 def mark_as_read(self):
4779 self.read = True
4785 self.read = True
4780 Session().add(self)
4786 Session().add(self)
4781
4787
4782
4788
4783 class UserNotice(Base, BaseModel):
4789 class UserNotice(Base, BaseModel):
4784 __tablename__ = 'user_notices'
4790 __tablename__ = 'user_notices'
4785 __table_args__ = (
4791 __table_args__ = (
4786 base_table_args
4792 base_table_args
4787 )
4793 )
4788
4794
4789 NOTIFICATION_TYPE_MESSAGE = 'message'
4795 NOTIFICATION_TYPE_MESSAGE = 'message'
4790 NOTIFICATION_TYPE_NOTICE = 'notice'
4796 NOTIFICATION_TYPE_NOTICE = 'notice'
4791
4797
4792 NOTIFICATION_LEVEL_INFO = 'info'
4798 NOTIFICATION_LEVEL_INFO = 'info'
4793 NOTIFICATION_LEVEL_WARNING = 'warning'
4799 NOTIFICATION_LEVEL_WARNING = 'warning'
4794 NOTIFICATION_LEVEL_ERROR = 'error'
4800 NOTIFICATION_LEVEL_ERROR = 'error'
4795
4801
4796 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4802 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4797
4803
4798 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4804 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4799 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4805 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4800
4806
4801 notice_read = Column('notice_read', Boolean, default=False)
4807 notice_read = Column('notice_read', Boolean, default=False)
4802
4808
4803 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4809 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4804 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4810 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4805
4811
4806 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4812 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4807 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4813 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4808
4814
4809 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4815 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4810 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4816 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4811
4817
4812 @classmethod
4818 @classmethod
4813 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4819 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4814
4820
4815 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4821 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4816 cls.NOTIFICATION_LEVEL_WARNING,
4822 cls.NOTIFICATION_LEVEL_WARNING,
4817 cls.NOTIFICATION_LEVEL_INFO]:
4823 cls.NOTIFICATION_LEVEL_INFO]:
4818 return
4824 return
4819
4825
4820 from rhodecode.model.user import UserModel
4826 from rhodecode.model.user import UserModel
4821 user = UserModel().get_user(user)
4827 user = UserModel().get_user(user)
4822
4828
4823 new_notice = UserNotice()
4829 new_notice = UserNotice()
4824 if not allow_duplicate:
4830 if not allow_duplicate:
4825 existing_msg = UserNotice().query() \
4831 existing_msg = UserNotice().query() \
4826 .filter(UserNotice.user == user) \
4832 .filter(UserNotice.user == user) \
4827 .filter(UserNotice.notice_body == body) \
4833 .filter(UserNotice.notice_body == body) \
4828 .filter(UserNotice.notice_read == false()) \
4834 .filter(UserNotice.notice_read == false()) \
4829 .scalar()
4835 .scalar()
4830 if existing_msg:
4836 if existing_msg:
4831 log.warning('Ignoring duplicate notice for user %s', user)
4837 log.warning('Ignoring duplicate notice for user %s', user)
4832 return
4838 return
4833
4839
4834 new_notice.user = user
4840 new_notice.user = user
4835 new_notice.notice_subject = subject
4841 new_notice.notice_subject = subject
4836 new_notice.notice_body = body
4842 new_notice.notice_body = body
4837 new_notice.notification_level = notice_level
4843 new_notice.notification_level = notice_level
4838 Session().add(new_notice)
4844 Session().add(new_notice)
4839 Session().commit()
4845 Session().commit()
4840
4846
4841
4847
4842 class Gist(Base, BaseModel):
4848 class Gist(Base, BaseModel):
4843 __tablename__ = 'gists'
4849 __tablename__ = 'gists'
4844 __table_args__ = (
4850 __table_args__ = (
4845 Index('g_gist_access_id_idx', 'gist_access_id'),
4851 Index('g_gist_access_id_idx', 'gist_access_id'),
4846 Index('g_created_on_idx', 'created_on'),
4852 Index('g_created_on_idx', 'created_on'),
4847 base_table_args
4853 base_table_args
4848 )
4854 )
4849
4855
4850 GIST_PUBLIC = 'public'
4856 GIST_PUBLIC = 'public'
4851 GIST_PRIVATE = 'private'
4857 GIST_PRIVATE = 'private'
4852 DEFAULT_FILENAME = 'gistfile1.txt'
4858 DEFAULT_FILENAME = 'gistfile1.txt'
4853
4859
4854 ACL_LEVEL_PUBLIC = 'acl_public'
4860 ACL_LEVEL_PUBLIC = 'acl_public'
4855 ACL_LEVEL_PRIVATE = 'acl_private'
4861 ACL_LEVEL_PRIVATE = 'acl_private'
4856
4862
4857 gist_id = Column('gist_id', Integer(), primary_key=True)
4863 gist_id = Column('gist_id', Integer(), primary_key=True)
4858 gist_access_id = Column('gist_access_id', Unicode(250))
4864 gist_access_id = Column('gist_access_id', Unicode(250))
4859 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4865 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4860 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4866 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4861 gist_expires = Column('gist_expires', Float(53), nullable=False)
4867 gist_expires = Column('gist_expires', Float(53), nullable=False)
4862 gist_type = Column('gist_type', Unicode(128), nullable=False)
4868 gist_type = Column('gist_type', Unicode(128), nullable=False)
4863 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4869 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4864 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4870 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4865 acl_level = Column('acl_level', Unicode(128), nullable=True)
4871 acl_level = Column('acl_level', Unicode(128), nullable=True)
4866
4872
4867 owner = relationship('User', back_populates='user_gists')
4873 owner = relationship('User', back_populates='user_gists')
4868
4874
4869 def __repr__(self):
4875 def __repr__(self):
4870 return f'<Gist:[{self.gist_type}]{self.gist_access_id}>'
4876 return f'<Gist:[{self.gist_type}]{self.gist_access_id}>'
4871
4877
4872 @hybrid_property
4878 @hybrid_property
4873 def description_safe(self):
4879 def description_safe(self):
4874 from rhodecode.lib import helpers as h
4880 from rhodecode.lib import helpers as h
4875 return h.escape(self.gist_description)
4881 return h.escape(self.gist_description)
4876
4882
4877 @classmethod
4883 @classmethod
4878 def get_or_404(cls, id_):
4884 def get_or_404(cls, id_):
4879 from pyramid.httpexceptions import HTTPNotFound
4885 from pyramid.httpexceptions import HTTPNotFound
4880
4886
4881 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4887 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4882 if not res:
4888 if not res:
4883 log.debug('WARN: No DB entry with id %s', id_)
4889 log.debug('WARN: No DB entry with id %s', id_)
4884 raise HTTPNotFound()
4890 raise HTTPNotFound()
4885 return res
4891 return res
4886
4892
4887 @classmethod
4893 @classmethod
4888 def get_by_access_id(cls, gist_access_id):
4894 def get_by_access_id(cls, gist_access_id):
4889 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4895 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4890
4896
4891 def gist_url(self):
4897 def gist_url(self):
4892 from rhodecode.model.gist import GistModel
4898 from rhodecode.model.gist import GistModel
4893 return GistModel().get_url(self)
4899 return GistModel().get_url(self)
4894
4900
4895 @classmethod
4901 @classmethod
4896 def base_path(cls):
4902 def base_path(cls):
4897 """
4903 """
4898 Returns base path when all gists are stored
4904 Returns base path when all gists are stored
4899
4905
4900 :param cls:
4906 :param cls:
4901 """
4907 """
4902 from rhodecode.model.gist import GIST_STORE_LOC
4908 from rhodecode.model.gist import GIST_STORE_LOC
4903 q = Session().query(RhodeCodeUi)\
4909 q = Session().query(RhodeCodeUi)\
4904 .filter(RhodeCodeUi.ui_key == URL_SEP)
4910 .filter(RhodeCodeUi.ui_key == URL_SEP)
4905 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4911 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4906 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4912 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4907
4913
4908 def get_api_data(self):
4914 def get_api_data(self):
4909 """
4915 """
4910 Common function for generating gist related data for API
4916 Common function for generating gist related data for API
4911 """
4917 """
4912 gist = self
4918 gist = self
4913 data = {
4919 data = {
4914 'gist_id': gist.gist_id,
4920 'gist_id': gist.gist_id,
4915 'type': gist.gist_type,
4921 'type': gist.gist_type,
4916 'access_id': gist.gist_access_id,
4922 'access_id': gist.gist_access_id,
4917 'description': gist.gist_description,
4923 'description': gist.gist_description,
4918 'url': gist.gist_url(),
4924 'url': gist.gist_url(),
4919 'expires': gist.gist_expires,
4925 'expires': gist.gist_expires,
4920 'created_on': gist.created_on,
4926 'created_on': gist.created_on,
4921 'modified_at': gist.modified_at,
4927 'modified_at': gist.modified_at,
4922 'content': None,
4928 'content': None,
4923 'acl_level': gist.acl_level,
4929 'acl_level': gist.acl_level,
4924 }
4930 }
4925 return data
4931 return data
4926
4932
4927 def __json__(self):
4933 def __json__(self):
4928 data = dict(
4934 data = dict(
4929 )
4935 )
4930 data.update(self.get_api_data())
4936 data.update(self.get_api_data())
4931 return data
4937 return data
4932 # SCM functions
4938 # SCM functions
4933
4939
4934 def scm_instance(self, **kwargs):
4940 def scm_instance(self, **kwargs):
4935 """
4941 """
4936 Get an instance of VCS Repository
4942 Get an instance of VCS Repository
4937
4943
4938 :param kwargs:
4944 :param kwargs:
4939 """
4945 """
4940 from rhodecode.model.gist import GistModel
4946 from rhodecode.model.gist import GistModel
4941 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4947 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4942 return get_vcs_instance(
4948 return get_vcs_instance(
4943 repo_path=safe_str(full_repo_path), create=False,
4949 repo_path=safe_str(full_repo_path), create=False,
4944 _vcs_alias=GistModel.vcs_backend)
4950 _vcs_alias=GistModel.vcs_backend)
4945
4951
4946
4952
4947 class ExternalIdentity(Base, BaseModel):
4953 class ExternalIdentity(Base, BaseModel):
4948 __tablename__ = 'external_identities'
4954 __tablename__ = 'external_identities'
4949 __table_args__ = (
4955 __table_args__ = (
4950 Index('local_user_id_idx', 'local_user_id'),
4956 Index('local_user_id_idx', 'local_user_id'),
4951 Index('external_id_idx', 'external_id'),
4957 Index('external_id_idx', 'external_id'),
4952 base_table_args
4958 base_table_args
4953 )
4959 )
4954
4960
4955 external_id = Column('external_id', Unicode(255), default='', primary_key=True)
4961 external_id = Column('external_id', Unicode(255), default='', primary_key=True)
4956 external_username = Column('external_username', Unicode(1024), default='')
4962 external_username = Column('external_username', Unicode(1024), default='')
4957 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4963 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4958 provider_name = Column('provider_name', Unicode(255), default='', primary_key=True)
4964 provider_name = Column('provider_name', Unicode(255), default='', primary_key=True)
4959 access_token = Column('access_token', String(1024), default='')
4965 access_token = Column('access_token', String(1024), default='')
4960 alt_token = Column('alt_token', String(1024), default='')
4966 alt_token = Column('alt_token', String(1024), default='')
4961 token_secret = Column('token_secret', String(1024), default='')
4967 token_secret = Column('token_secret', String(1024), default='')
4962
4968
4963 @classmethod
4969 @classmethod
4964 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4970 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4965 """
4971 """
4966 Returns ExternalIdentity instance based on search params
4972 Returns ExternalIdentity instance based on search params
4967
4973
4968 :param external_id:
4974 :param external_id:
4969 :param provider_name:
4975 :param provider_name:
4970 :return: ExternalIdentity
4976 :return: ExternalIdentity
4971 """
4977 """
4972 query = cls.query()
4978 query = cls.query()
4973 query = query.filter(cls.external_id == external_id)
4979 query = query.filter(cls.external_id == external_id)
4974 query = query.filter(cls.provider_name == provider_name)
4980 query = query.filter(cls.provider_name == provider_name)
4975 if local_user_id:
4981 if local_user_id:
4976 query = query.filter(cls.local_user_id == local_user_id)
4982 query = query.filter(cls.local_user_id == local_user_id)
4977 return query.first()
4983 return query.first()
4978
4984
4979 @classmethod
4985 @classmethod
4980 def user_by_external_id_and_provider(cls, external_id, provider_name):
4986 def user_by_external_id_and_provider(cls, external_id, provider_name):
4981 """
4987 """
4982 Returns User instance based on search params
4988 Returns User instance based on search params
4983
4989
4984 :param external_id:
4990 :param external_id:
4985 :param provider_name:
4991 :param provider_name:
4986 :return: User
4992 :return: User
4987 """
4993 """
4988 query = User.query()
4994 query = User.query()
4989 query = query.filter(cls.external_id == external_id)
4995 query = query.filter(cls.external_id == external_id)
4990 query = query.filter(cls.provider_name == provider_name)
4996 query = query.filter(cls.provider_name == provider_name)
4991 query = query.filter(User.user_id == cls.local_user_id)
4997 query = query.filter(User.user_id == cls.local_user_id)
4992 return query.first()
4998 return query.first()
4993
4999
4994 @classmethod
5000 @classmethod
4995 def by_local_user_id(cls, local_user_id):
5001 def by_local_user_id(cls, local_user_id):
4996 """
5002 """
4997 Returns all tokens for user
5003 Returns all tokens for user
4998
5004
4999 :param local_user_id:
5005 :param local_user_id:
5000 :return: ExternalIdentity
5006 :return: ExternalIdentity
5001 """
5007 """
5002 query = cls.query()
5008 query = cls.query()
5003 query = query.filter(cls.local_user_id == local_user_id)
5009 query = query.filter(cls.local_user_id == local_user_id)
5004 return query
5010 return query
5005
5011
5006 @classmethod
5012 @classmethod
5007 def load_provider_plugin(cls, plugin_id):
5013 def load_provider_plugin(cls, plugin_id):
5008 from rhodecode.authentication.base import loadplugin
5014 from rhodecode.authentication.base import loadplugin
5009 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
5015 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
5010 auth_plugin = loadplugin(_plugin_id)
5016 auth_plugin = loadplugin(_plugin_id)
5011 return auth_plugin
5017 return auth_plugin
5012
5018
5013
5019
5014 class Integration(Base, BaseModel):
5020 class Integration(Base, BaseModel):
5015 __tablename__ = 'integrations'
5021 __tablename__ = 'integrations'
5016 __table_args__ = (
5022 __table_args__ = (
5017 base_table_args
5023 base_table_args
5018 )
5024 )
5019
5025
5020 integration_id = Column('integration_id', Integer(), primary_key=True)
5026 integration_id = Column('integration_id', Integer(), primary_key=True)
5021 integration_type = Column('integration_type', String(255))
5027 integration_type = Column('integration_type', String(255))
5022 enabled = Column('enabled', Boolean(), nullable=False)
5028 enabled = Column('enabled', Boolean(), nullable=False)
5023 name = Column('name', String(255), nullable=False)
5029 name = Column('name', String(255), nullable=False)
5024 child_repos_only = Column('child_repos_only', Boolean(), nullable=False, default=False)
5030 child_repos_only = Column('child_repos_only', Boolean(), nullable=False, default=False)
5025
5031
5026 settings = Column(
5032 settings = Column(
5027 'settings_json', MutationObj.as_mutable(
5033 'settings_json', MutationObj.as_mutable(
5028 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
5034 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
5029 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
5035 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
5030 repo = relationship('Repository', lazy='joined', back_populates='integrations')
5036 repo = relationship('Repository', lazy='joined', back_populates='integrations')
5031
5037
5032 repo_group_id = Column('repo_group_id', Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
5038 repo_group_id = Column('repo_group_id', Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
5033 repo_group = relationship('RepoGroup', lazy='joined', back_populates='integrations')
5039 repo_group = relationship('RepoGroup', lazy='joined', back_populates='integrations')
5034
5040
5035 @property
5041 @property
5036 def scope(self):
5042 def scope(self):
5037 if self.repo:
5043 if self.repo:
5038 return repr(self.repo)
5044 return repr(self.repo)
5039 if self.repo_group:
5045 if self.repo_group:
5040 if self.child_repos_only:
5046 if self.child_repos_only:
5041 return repr(self.repo_group) + ' (child repos only)'
5047 return repr(self.repo_group) + ' (child repos only)'
5042 else:
5048 else:
5043 return repr(self.repo_group) + ' (recursive)'
5049 return repr(self.repo_group) + ' (recursive)'
5044 if self.child_repos_only:
5050 if self.child_repos_only:
5045 return 'root_repos'
5051 return 'root_repos'
5046 return 'global'
5052 return 'global'
5047
5053
5048 def __repr__(self):
5054 def __repr__(self):
5049 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
5055 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
5050
5056
5051
5057
5052 class RepoReviewRuleUser(Base, BaseModel):
5058 class RepoReviewRuleUser(Base, BaseModel):
5053 __tablename__ = 'repo_review_rules_users'
5059 __tablename__ = 'repo_review_rules_users'
5054 __table_args__ = (
5060 __table_args__ = (
5055 base_table_args
5061 base_table_args
5056 )
5062 )
5057 ROLE_REVIEWER = 'reviewer'
5063 ROLE_REVIEWER = 'reviewer'
5058 ROLE_OBSERVER = 'observer'
5064 ROLE_OBSERVER = 'observer'
5059 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5065 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5060
5066
5061 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
5067 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
5062 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5068 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5063 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
5069 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
5064 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5070 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5065 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5071 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5066 user = relationship('User', back_populates='user_review_rules')
5072 user = relationship('User', back_populates='user_review_rules')
5067
5073
5068 def rule_data(self):
5074 def rule_data(self):
5069 return {
5075 return {
5070 'mandatory': self.mandatory,
5076 'mandatory': self.mandatory,
5071 'role': self.role,
5077 'role': self.role,
5072 }
5078 }
5073
5079
5074
5080
5075 class RepoReviewRuleUserGroup(Base, BaseModel):
5081 class RepoReviewRuleUserGroup(Base, BaseModel):
5076 __tablename__ = 'repo_review_rules_users_groups'
5082 __tablename__ = 'repo_review_rules_users_groups'
5077 __table_args__ = (
5083 __table_args__ = (
5078 base_table_args
5084 base_table_args
5079 )
5085 )
5080
5086
5081 VOTE_RULE_ALL = -1
5087 VOTE_RULE_ALL = -1
5082 ROLE_REVIEWER = 'reviewer'
5088 ROLE_REVIEWER = 'reviewer'
5083 ROLE_OBSERVER = 'observer'
5089 ROLE_OBSERVER = 'observer'
5084 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5090 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5085
5091
5086 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
5092 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
5087 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5093 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5088 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
5094 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
5089 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5095 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5090 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5096 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5091 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5097 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5092 users_group = relationship('UserGroup')
5098 users_group = relationship('UserGroup')
5093
5099
5094 def rule_data(self):
5100 def rule_data(self):
5095 return {
5101 return {
5096 'mandatory': self.mandatory,
5102 'mandatory': self.mandatory,
5097 'role': self.role,
5103 'role': self.role,
5098 'vote_rule': self.vote_rule
5104 'vote_rule': self.vote_rule
5099 }
5105 }
5100
5106
5101 @property
5107 @property
5102 def vote_rule_label(self):
5108 def vote_rule_label(self):
5103 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
5109 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
5104 return 'all must vote'
5110 return 'all must vote'
5105 else:
5111 else:
5106 return 'min. vote {}'.format(self.vote_rule)
5112 return 'min. vote {}'.format(self.vote_rule)
5107
5113
5108
5114
5109 class RepoReviewRule(Base, BaseModel):
5115 class RepoReviewRule(Base, BaseModel):
5110 __tablename__ = 'repo_review_rules'
5116 __tablename__ = 'repo_review_rules'
5111 __table_args__ = (
5117 __table_args__ = (
5112 base_table_args
5118 base_table_args
5113 )
5119 )
5114
5120
5115 repo_review_rule_id = Column(
5121 repo_review_rule_id = Column(
5116 'repo_review_rule_id', Integer(), primary_key=True)
5122 'repo_review_rule_id', Integer(), primary_key=True)
5117 repo_id = Column(
5123 repo_id = Column(
5118 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
5124 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
5119 repo = relationship('Repository', back_populates='review_rules')
5125 repo = relationship('Repository', back_populates='review_rules')
5120
5126
5121 review_rule_name = Column('review_rule_name', String(255))
5127 review_rule_name = Column('review_rule_name', String(255))
5122 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5128 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5123 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5129 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5124 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5130 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5125
5131
5126 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5132 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5127
5133
5128 # Legacy fields, just for backward compat
5134 # Legacy fields, just for backward compat
5129 _forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5135 _forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5130 _forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5136 _forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5131
5137
5132 pr_author = Column("pr_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5138 pr_author = Column("pr_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5133 commit_author = Column("commit_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5139 commit_author = Column("commit_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5134
5140
5135 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5141 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5136
5142
5137 rule_users = relationship('RepoReviewRuleUser')
5143 rule_users = relationship('RepoReviewRuleUser')
5138 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5144 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5139
5145
5140 def _validate_pattern(self, value):
5146 def _validate_pattern(self, value):
5141 re.compile('^' + glob2re(value) + '$')
5147 re.compile('^' + glob2re(value) + '$')
5142
5148
5143 @hybrid_property
5149 @hybrid_property
5144 def source_branch_pattern(self):
5150 def source_branch_pattern(self):
5145 return self._branch_pattern or '*'
5151 return self._branch_pattern or '*'
5146
5152
5147 @source_branch_pattern.setter
5153 @source_branch_pattern.setter
5148 def source_branch_pattern(self, value):
5154 def source_branch_pattern(self, value):
5149 self._validate_pattern(value)
5155 self._validate_pattern(value)
5150 self._branch_pattern = value or '*'
5156 self._branch_pattern = value or '*'
5151
5157
5152 @hybrid_property
5158 @hybrid_property
5153 def target_branch_pattern(self):
5159 def target_branch_pattern(self):
5154 return self._target_branch_pattern or '*'
5160 return self._target_branch_pattern or '*'
5155
5161
5156 @target_branch_pattern.setter
5162 @target_branch_pattern.setter
5157 def target_branch_pattern(self, value):
5163 def target_branch_pattern(self, value):
5158 self._validate_pattern(value)
5164 self._validate_pattern(value)
5159 self._target_branch_pattern = value or '*'
5165 self._target_branch_pattern = value or '*'
5160
5166
5161 @hybrid_property
5167 @hybrid_property
5162 def file_pattern(self):
5168 def file_pattern(self):
5163 return self._file_pattern or '*'
5169 return self._file_pattern or '*'
5164
5170
5165 @file_pattern.setter
5171 @file_pattern.setter
5166 def file_pattern(self, value):
5172 def file_pattern(self, value):
5167 self._validate_pattern(value)
5173 self._validate_pattern(value)
5168 self._file_pattern = value or '*'
5174 self._file_pattern = value or '*'
5169
5175
5170 @hybrid_property
5176 @hybrid_property
5171 def forbid_pr_author_to_review(self):
5177 def forbid_pr_author_to_review(self):
5172 return self.pr_author == 'forbid_pr_author'
5178 return self.pr_author == 'forbid_pr_author'
5173
5179
5174 @hybrid_property
5180 @hybrid_property
5175 def include_pr_author_to_review(self):
5181 def include_pr_author_to_review(self):
5176 return self.pr_author == 'include_pr_author'
5182 return self.pr_author == 'include_pr_author'
5177
5183
5178 @hybrid_property
5184 @hybrid_property
5179 def forbid_commit_author_to_review(self):
5185 def forbid_commit_author_to_review(self):
5180 return self.commit_author == 'forbid_commit_author'
5186 return self.commit_author == 'forbid_commit_author'
5181
5187
5182 @hybrid_property
5188 @hybrid_property
5183 def include_commit_author_to_review(self):
5189 def include_commit_author_to_review(self):
5184 return self.commit_author == 'include_commit_author'
5190 return self.commit_author == 'include_commit_author'
5185
5191
5186 def matches(self, source_branch, target_branch, files_changed):
5192 def matches(self, source_branch, target_branch, files_changed):
5187 """
5193 """
5188 Check if this review rule matches a branch/files in a pull request
5194 Check if this review rule matches a branch/files in a pull request
5189
5195
5190 :param source_branch: source branch name for the commit
5196 :param source_branch: source branch name for the commit
5191 :param target_branch: target branch name for the commit
5197 :param target_branch: target branch name for the commit
5192 :param files_changed: list of file paths changed in the pull request
5198 :param files_changed: list of file paths changed in the pull request
5193 """
5199 """
5194
5200
5195 source_branch = source_branch or ''
5201 source_branch = source_branch or ''
5196 target_branch = target_branch or ''
5202 target_branch = target_branch or ''
5197 files_changed = files_changed or []
5203 files_changed = files_changed or []
5198
5204
5199 branch_matches = True
5205 branch_matches = True
5200 if source_branch or target_branch:
5206 if source_branch or target_branch:
5201 if self.source_branch_pattern == '*':
5207 if self.source_branch_pattern == '*':
5202 source_branch_match = True
5208 source_branch_match = True
5203 else:
5209 else:
5204 if self.source_branch_pattern.startswith('re:'):
5210 if self.source_branch_pattern.startswith('re:'):
5205 source_pattern = self.source_branch_pattern[3:]
5211 source_pattern = self.source_branch_pattern[3:]
5206 else:
5212 else:
5207 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5213 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5208 source_branch_regex = re.compile(source_pattern)
5214 source_branch_regex = re.compile(source_pattern)
5209 source_branch_match = bool(source_branch_regex.search(source_branch))
5215 source_branch_match = bool(source_branch_regex.search(source_branch))
5210 if self.target_branch_pattern == '*':
5216 if self.target_branch_pattern == '*':
5211 target_branch_match = True
5217 target_branch_match = True
5212 else:
5218 else:
5213 if self.target_branch_pattern.startswith('re:'):
5219 if self.target_branch_pattern.startswith('re:'):
5214 target_pattern = self.target_branch_pattern[3:]
5220 target_pattern = self.target_branch_pattern[3:]
5215 else:
5221 else:
5216 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5222 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5217 target_branch_regex = re.compile(target_pattern)
5223 target_branch_regex = re.compile(target_pattern)
5218 target_branch_match = bool(target_branch_regex.search(target_branch))
5224 target_branch_match = bool(target_branch_regex.search(target_branch))
5219
5225
5220 branch_matches = source_branch_match and target_branch_match
5226 branch_matches = source_branch_match and target_branch_match
5221
5227
5222 files_matches = True
5228 files_matches = True
5223 if self.file_pattern != '*':
5229 if self.file_pattern != '*':
5224 files_matches = False
5230 files_matches = False
5225 if self.file_pattern.startswith('re:'):
5231 if self.file_pattern.startswith('re:'):
5226 file_pattern = self.file_pattern[3:]
5232 file_pattern = self.file_pattern[3:]
5227 else:
5233 else:
5228 file_pattern = glob2re(self.file_pattern)
5234 file_pattern = glob2re(self.file_pattern)
5229 file_regex = re.compile(file_pattern)
5235 file_regex = re.compile(file_pattern)
5230 for file_data in files_changed:
5236 for file_data in files_changed:
5231 filename = file_data.get('filename')
5237 filename = file_data.get('filename')
5232
5238
5233 if file_regex.search(filename):
5239 if file_regex.search(filename):
5234 files_matches = True
5240 files_matches = True
5235 break
5241 break
5236
5242
5237 return branch_matches and files_matches
5243 return branch_matches and files_matches
5238
5244
5239 @property
5245 @property
5240 def review_users(self):
5246 def review_users(self):
5241 """ Returns the users which this rule applies to """
5247 """ Returns the users which this rule applies to """
5242
5248
5243 users = collections.OrderedDict()
5249 users = collections.OrderedDict()
5244
5250
5245 for rule_user in self.rule_users:
5251 for rule_user in self.rule_users:
5246 if rule_user.user.active:
5252 if rule_user.user.active:
5247 if rule_user.user not in users:
5253 if rule_user.user not in users:
5248 users[rule_user.user.username] = {
5254 users[rule_user.user.username] = {
5249 'user': rule_user.user,
5255 'user': rule_user.user,
5250 'source': 'user',
5256 'source': 'user',
5251 'source_data': {},
5257 'source_data': {},
5252 'data': rule_user.rule_data()
5258 'data': rule_user.rule_data()
5253 }
5259 }
5254
5260
5255 for rule_user_group in self.rule_user_groups:
5261 for rule_user_group in self.rule_user_groups:
5256 source_data = {
5262 source_data = {
5257 'user_group_id': rule_user_group.users_group.users_group_id,
5263 'user_group_id': rule_user_group.users_group.users_group_id,
5258 'name': rule_user_group.users_group.users_group_name,
5264 'name': rule_user_group.users_group.users_group_name,
5259 'members': len(rule_user_group.users_group.members)
5265 'members': len(rule_user_group.users_group.members)
5260 }
5266 }
5261 for member in rule_user_group.users_group.members:
5267 for member in rule_user_group.users_group.members:
5262 if member.user.active:
5268 if member.user.active:
5263 key = member.user.username
5269 key = member.user.username
5264 if key in users:
5270 if key in users:
5265 # skip this member as we have him already
5271 # skip this member as we have him already
5266 # this prevents from override the "first" matched
5272 # this prevents from override the "first" matched
5267 # users with duplicates in multiple groups
5273 # users with duplicates in multiple groups
5268 continue
5274 continue
5269
5275
5270 users[key] = {
5276 users[key] = {
5271 'user': member.user,
5277 'user': member.user,
5272 'source': 'user_group',
5278 'source': 'user_group',
5273 'source_data': source_data,
5279 'source_data': source_data,
5274 'data': rule_user_group.rule_data()
5280 'data': rule_user_group.rule_data()
5275 }
5281 }
5276
5282
5277 return users
5283 return users
5278
5284
5279 def user_group_vote_rule(self, user_id):
5285 def user_group_vote_rule(self, user_id):
5280
5286
5281 rules = []
5287 rules = []
5282 if not self.rule_user_groups:
5288 if not self.rule_user_groups:
5283 return rules
5289 return rules
5284
5290
5285 for user_group in self.rule_user_groups:
5291 for user_group in self.rule_user_groups:
5286 user_group_members = [x.user_id for x in user_group.users_group.members]
5292 user_group_members = [x.user_id for x in user_group.users_group.members]
5287 if user_id in user_group_members:
5293 if user_id in user_group_members:
5288 rules.append(user_group)
5294 rules.append(user_group)
5289 return rules
5295 return rules
5290
5296
5291 def __repr__(self):
5297 def __repr__(self):
5292 return f'<RepoReviewerRule(id={self.repo_review_rule_id}, repo={self.repo!r})>'
5298 return f'<RepoReviewerRule(id={self.repo_review_rule_id}, repo={self.repo!r})>'
5293
5299
5294
5300
5295 class ScheduleEntry(Base, BaseModel):
5301 class ScheduleEntry(Base, BaseModel):
5296 __tablename__ = 'schedule_entries'
5302 __tablename__ = 'schedule_entries'
5297 __table_args__ = (
5303 __table_args__ = (
5298 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5304 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5299 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5305 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5300 base_table_args,
5306 base_table_args,
5301 )
5307 )
5302 SCHEDULE_TYPE_INTEGER = "integer"
5308 SCHEDULE_TYPE_INTEGER = "integer"
5303 SCHEDULE_TYPE_CRONTAB = "crontab"
5309 SCHEDULE_TYPE_CRONTAB = "crontab"
5304
5310
5305 schedule_types = [SCHEDULE_TYPE_CRONTAB, SCHEDULE_TYPE_INTEGER]
5311 schedule_types = [SCHEDULE_TYPE_CRONTAB, SCHEDULE_TYPE_INTEGER]
5306 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5312 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5307
5313
5308 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5314 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5309 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5315 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5310 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5316 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5311
5317
5312 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5318 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5313 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5319 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5314
5320
5315 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5321 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5316 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5322 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5317
5323
5318 # task
5324 # task
5319 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5325 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5320 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5326 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5321 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5327 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5322 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5328 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5323
5329
5324 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5330 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5325 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5331 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5326
5332
5327 @hybrid_property
5333 @hybrid_property
5328 def schedule_type(self):
5334 def schedule_type(self):
5329 return self._schedule_type
5335 return self._schedule_type
5330
5336
5331 @schedule_type.setter
5337 @schedule_type.setter
5332 def schedule_type(self, val):
5338 def schedule_type(self, val):
5333 if val not in self.schedule_types:
5339 if val not in self.schedule_types:
5334 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5340 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5335 val, self.schedule_type))
5341 val, self.schedule_type))
5336
5342
5337 self._schedule_type = val
5343 self._schedule_type = val
5338
5344
5339 @classmethod
5345 @classmethod
5340 def get_uid(cls, obj):
5346 def get_uid(cls, obj):
5341 args = obj.task_args
5347 args = obj.task_args
5342 kwargs = obj.task_kwargs
5348 kwargs = obj.task_kwargs
5343 if isinstance(args, JsonRaw):
5349 if isinstance(args, JsonRaw):
5344 try:
5350 try:
5345 args = json.loads(args)
5351 args = json.loads(args)
5346 except ValueError:
5352 except ValueError:
5347 args = tuple()
5353 args = tuple()
5348
5354
5349 if isinstance(kwargs, JsonRaw):
5355 if isinstance(kwargs, JsonRaw):
5350 try:
5356 try:
5351 kwargs = json.loads(kwargs)
5357 kwargs = json.loads(kwargs)
5352 except ValueError:
5358 except ValueError:
5353 kwargs = dict()
5359 kwargs = dict()
5354
5360
5355 dot_notation = obj.task_dot_notation
5361 dot_notation = obj.task_dot_notation
5356 val = '.'.join(map(safe_str, [
5362 val = '.'.join(map(safe_str, [
5357 sorted(dot_notation), args, sorted(kwargs.items())]))
5363 sorted(dot_notation), args, sorted(kwargs.items())]))
5358 return sha1(safe_bytes(val))
5364 return sha1(safe_bytes(val))
5359
5365
5360 @classmethod
5366 @classmethod
5361 def get_by_schedule_name(cls, schedule_name):
5367 def get_by_schedule_name(cls, schedule_name):
5362 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5368 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5363
5369
5364 @classmethod
5370 @classmethod
5365 def get_by_schedule_id(cls, schedule_id):
5371 def get_by_schedule_id(cls, schedule_id):
5366 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5372 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5367
5373
5368 @property
5374 @property
5369 def task(self):
5375 def task(self):
5370 return self.task_dot_notation
5376 return self.task_dot_notation
5371
5377
5372 @property
5378 @property
5373 def schedule(self):
5379 def schedule(self):
5374 from rhodecode.lib.celerylib.utils import raw_2_schedule
5380 from rhodecode.lib.celerylib.utils import raw_2_schedule
5375 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5381 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5376 return schedule
5382 return schedule
5377
5383
5378 @property
5384 @property
5379 def args(self):
5385 def args(self):
5380 try:
5386 try:
5381 return list(self.task_args or [])
5387 return list(self.task_args or [])
5382 except ValueError:
5388 except ValueError:
5383 return list()
5389 return list()
5384
5390
5385 @property
5391 @property
5386 def kwargs(self):
5392 def kwargs(self):
5387 try:
5393 try:
5388 return dict(self.task_kwargs or {})
5394 return dict(self.task_kwargs or {})
5389 except ValueError:
5395 except ValueError:
5390 return dict()
5396 return dict()
5391
5397
5392 def _as_raw(self, val, indent=False):
5398 def _as_raw(self, val, indent=False):
5393 if hasattr(val, 'de_coerce'):
5399 if hasattr(val, 'de_coerce'):
5394 val = val.de_coerce()
5400 val = val.de_coerce()
5395 if val:
5401 if val:
5396 if indent:
5402 if indent:
5397 val = ext_json.formatted_str_json(val)
5403 val = ext_json.formatted_str_json(val)
5398 else:
5404 else:
5399 val = ext_json.str_json(val)
5405 val = ext_json.str_json(val)
5400
5406
5401 return val
5407 return val
5402
5408
5403 @property
5409 @property
5404 def schedule_definition_raw(self):
5410 def schedule_definition_raw(self):
5405 return self._as_raw(self.schedule_definition)
5411 return self._as_raw(self.schedule_definition)
5406
5412
5407 def args_raw(self, indent=False):
5413 def args_raw(self, indent=False):
5408 return self._as_raw(self.task_args, indent)
5414 return self._as_raw(self.task_args, indent)
5409
5415
5410 def kwargs_raw(self, indent=False):
5416 def kwargs_raw(self, indent=False):
5411 return self._as_raw(self.task_kwargs, indent)
5417 return self._as_raw(self.task_kwargs, indent)
5412
5418
5413 def __repr__(self):
5419 def __repr__(self):
5414 return f'<DB:ScheduleEntry({self.schedule_entry_id}:{self.schedule_name})>'
5420 return f'<DB:ScheduleEntry({self.schedule_entry_id}:{self.schedule_name})>'
5415
5421
5416
5422
5417 @event.listens_for(ScheduleEntry, 'before_update')
5423 @event.listens_for(ScheduleEntry, 'before_update')
5418 def update_task_uid(mapper, connection, target):
5424 def update_task_uid(mapper, connection, target):
5419 target.task_uid = ScheduleEntry.get_uid(target)
5425 target.task_uid = ScheduleEntry.get_uid(target)
5420
5426
5421
5427
5422 @event.listens_for(ScheduleEntry, 'before_insert')
5428 @event.listens_for(ScheduleEntry, 'before_insert')
5423 def set_task_uid(mapper, connection, target):
5429 def set_task_uid(mapper, connection, target):
5424 target.task_uid = ScheduleEntry.get_uid(target)
5430 target.task_uid = ScheduleEntry.get_uid(target)
5425
5431
5426
5432
5427 class _BaseBranchPerms(BaseModel):
5433 class _BaseBranchPerms(BaseModel):
5428 @classmethod
5434 @classmethod
5429 def compute_hash(cls, value):
5435 def compute_hash(cls, value):
5430 return sha1_safe(value)
5436 return sha1_safe(value)
5431
5437
5432 @hybrid_property
5438 @hybrid_property
5433 def branch_pattern(self):
5439 def branch_pattern(self):
5434 return self._branch_pattern or '*'
5440 return self._branch_pattern or '*'
5435
5441
5436 @hybrid_property
5442 @hybrid_property
5437 def branch_hash(self):
5443 def branch_hash(self):
5438 return self._branch_hash
5444 return self._branch_hash
5439
5445
5440 def _validate_glob(self, value):
5446 def _validate_glob(self, value):
5441 re.compile('^' + glob2re(value) + '$')
5447 re.compile('^' + glob2re(value) + '$')
5442
5448
5443 @branch_pattern.setter
5449 @branch_pattern.setter
5444 def branch_pattern(self, value):
5450 def branch_pattern(self, value):
5445 self._validate_glob(value)
5451 self._validate_glob(value)
5446 self._branch_pattern = value or '*'
5452 self._branch_pattern = value or '*'
5447 # set the Hash when setting the branch pattern
5453 # set the Hash when setting the branch pattern
5448 self._branch_hash = self.compute_hash(self._branch_pattern)
5454 self._branch_hash = self.compute_hash(self._branch_pattern)
5449
5455
5450 def matches(self, branch):
5456 def matches(self, branch):
5451 """
5457 """
5452 Check if this the branch matches entry
5458 Check if this the branch matches entry
5453
5459
5454 :param branch: branch name for the commit
5460 :param branch: branch name for the commit
5455 """
5461 """
5456
5462
5457 branch = branch or ''
5463 branch = branch or ''
5458
5464
5459 branch_matches = True
5465 branch_matches = True
5460 if branch:
5466 if branch:
5461 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5467 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5462 branch_matches = bool(branch_regex.search(branch))
5468 branch_matches = bool(branch_regex.search(branch))
5463
5469
5464 return branch_matches
5470 return branch_matches
5465
5471
5466
5472
5467 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5473 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5468 __tablename__ = 'user_to_repo_branch_permissions'
5474 __tablename__ = 'user_to_repo_branch_permissions'
5469 __table_args__ = (
5475 __table_args__ = (
5470 base_table_args
5476 base_table_args
5471 )
5477 )
5472
5478
5473 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5479 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5474
5480
5475 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5481 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5476 repo = relationship('Repository', back_populates='user_branch_perms')
5482 repo = relationship('Repository', back_populates='user_branch_perms')
5477
5483
5478 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5484 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5479 permission = relationship('Permission')
5485 permission = relationship('Permission')
5480
5486
5481 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5487 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5482 user_repo_to_perm = relationship('UserRepoToPerm', back_populates='branch_perm_entry')
5488 user_repo_to_perm = relationship('UserRepoToPerm', back_populates='branch_perm_entry')
5483
5489
5484 rule_order = Column('rule_order', Integer(), nullable=False)
5490 rule_order = Column('rule_order', Integer(), nullable=False)
5485 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5491 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5486 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5492 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5487
5493
5488 def __repr__(self):
5494 def __repr__(self):
5489 return f'<UserBranchPermission({self.user_repo_to_perm} => {self.branch_pattern!r})>'
5495 return f'<UserBranchPermission({self.user_repo_to_perm} => {self.branch_pattern!r})>'
5490
5496
5491
5497
5492 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5498 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5493 __tablename__ = 'user_group_to_repo_branch_permissions'
5499 __tablename__ = 'user_group_to_repo_branch_permissions'
5494 __table_args__ = (
5500 __table_args__ = (
5495 base_table_args
5501 base_table_args
5496 )
5502 )
5497
5503
5498 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5504 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5499
5505
5500 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5506 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5501 repo = relationship('Repository', back_populates='user_group_branch_perms')
5507 repo = relationship('Repository', back_populates='user_group_branch_perms')
5502
5508
5503 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5509 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5504 permission = relationship('Permission')
5510 permission = relationship('Permission')
5505
5511
5506 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None)
5512 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None)
5507 user_group_repo_to_perm = relationship('UserGroupRepoToPerm', back_populates='user_group_branch_perms')
5513 user_group_repo_to_perm = relationship('UserGroupRepoToPerm', back_populates='user_group_branch_perms')
5508
5514
5509 rule_order = Column('rule_order', Integer(), nullable=False)
5515 rule_order = Column('rule_order', Integer(), nullable=False)
5510 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5516 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5511 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5517 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5512
5518
5513 def __repr__(self):
5519 def __repr__(self):
5514 return f'<UserBranchPermission({self.user_group_repo_to_perm} => {self.branch_pattern!r})>'
5520 return f'<UserBranchPermission({self.user_group_repo_to_perm} => {self.branch_pattern!r})>'
5515
5521
5516
5522
5517 class UserBookmark(Base, BaseModel):
5523 class UserBookmark(Base, BaseModel):
5518 __tablename__ = 'user_bookmarks'
5524 __tablename__ = 'user_bookmarks'
5519 __table_args__ = (
5525 __table_args__ = (
5520 UniqueConstraint('user_id', 'bookmark_repo_id'),
5526 UniqueConstraint('user_id', 'bookmark_repo_id'),
5521 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5527 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5522 UniqueConstraint('user_id', 'bookmark_position'),
5528 UniqueConstraint('user_id', 'bookmark_position'),
5523 base_table_args
5529 base_table_args
5524 )
5530 )
5525
5531
5526 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5532 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5527 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5533 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5528 position = Column("bookmark_position", Integer(), nullable=False)
5534 position = Column("bookmark_position", Integer(), nullable=False)
5529 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5535 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5530 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5536 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5531 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5537 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5532
5538
5533 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5539 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5534 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5540 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5535
5541
5536 user = relationship("User")
5542 user = relationship("User")
5537
5543
5538 repository = relationship("Repository")
5544 repository = relationship("Repository")
5539 repository_group = relationship("RepoGroup")
5545 repository_group = relationship("RepoGroup")
5540
5546
5541 @classmethod
5547 @classmethod
5542 def get_by_position_for_user(cls, position, user_id):
5548 def get_by_position_for_user(cls, position, user_id):
5543 return cls.query() \
5549 return cls.query() \
5544 .filter(UserBookmark.user_id == user_id) \
5550 .filter(UserBookmark.user_id == user_id) \
5545 .filter(UserBookmark.position == position).scalar()
5551 .filter(UserBookmark.position == position).scalar()
5546
5552
5547 @classmethod
5553 @classmethod
5548 def get_bookmarks_for_user(cls, user_id, cache=True):
5554 def get_bookmarks_for_user(cls, user_id, cache=True):
5549 bookmarks = cls.query() \
5555 bookmarks = cls.query() \
5550 .filter(UserBookmark.user_id == user_id) \
5556 .filter(UserBookmark.user_id == user_id) \
5551 .options(joinedload(UserBookmark.repository)) \
5557 .options(joinedload(UserBookmark.repository)) \
5552 .options(joinedload(UserBookmark.repository_group)) \
5558 .options(joinedload(UserBookmark.repository_group)) \
5553 .order_by(UserBookmark.position.asc())
5559 .order_by(UserBookmark.position.asc())
5554
5560
5555 if cache:
5561 if cache:
5556 bookmarks = bookmarks.options(
5562 bookmarks = bookmarks.options(
5557 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5563 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5558 )
5564 )
5559
5565
5560 return bookmarks.all()
5566 return bookmarks.all()
5561
5567
5562 def __repr__(self):
5568 def __repr__(self):
5563 return f'<UserBookmark({self.position} @ {self.redirect_url!r})>'
5569 return f'<UserBookmark({self.position} @ {self.redirect_url!r})>'
5564
5570
5565
5571
5566 class FileStore(Base, BaseModel):
5572 class FileStore(Base, BaseModel):
5567 __tablename__ = 'file_store'
5573 __tablename__ = 'file_store'
5568 __table_args__ = (
5574 __table_args__ = (
5569 base_table_args
5575 base_table_args
5570 )
5576 )
5571
5577
5572 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5578 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5573 file_uid = Column('file_uid', String(1024), nullable=False)
5579 file_uid = Column('file_uid', String(1024), nullable=False)
5574 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5580 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5575 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5581 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5576 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5582 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5577
5583
5578 # sha256 hash
5584 # sha256 hash
5579 file_hash = Column('file_hash', String(512), nullable=False)
5585 file_hash = Column('file_hash', String(512), nullable=False)
5580 file_size = Column('file_size', BigInteger(), nullable=False)
5586 file_size = Column('file_size', BigInteger(), nullable=False)
5581
5587
5582 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5588 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5583 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5589 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5584 accessed_count = Column('accessed_count', Integer(), default=0)
5590 accessed_count = Column('accessed_count', Integer(), default=0)
5585
5591
5586 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5592 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5587
5593
5588 # if repo/repo_group reference is set, check for permissions
5594 # if repo/repo_group reference is set, check for permissions
5589 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5595 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5590
5596
5591 # hidden defines an attachment that should be hidden from showing in artifact listing
5597 # hidden defines an attachment that should be hidden from showing in artifact listing
5592 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5598 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5593
5599
5594 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5600 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5595 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id', back_populates='artifacts')
5601 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id', back_populates='artifacts')
5596
5602
5597 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5603 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5598
5604
5599 # scope limited to user, which requester have access to
5605 # scope limited to user, which requester have access to
5600 scope_user_id = Column(
5606 scope_user_id = Column(
5601 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5607 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5602 nullable=True, unique=None, default=None)
5608 nullable=True, unique=None, default=None)
5603 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id', back_populates='scope_artifacts')
5609 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id', back_populates='scope_artifacts')
5604
5610
5605 # scope limited to user group, which requester have access to
5611 # scope limited to user group, which requester have access to
5606 scope_user_group_id = Column(
5612 scope_user_group_id = Column(
5607 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5613 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5608 nullable=True, unique=None, default=None)
5614 nullable=True, unique=None, default=None)
5609 user_group = relationship('UserGroup', lazy='joined')
5615 user_group = relationship('UserGroup', lazy='joined')
5610
5616
5611 # scope limited to repo, which requester have access to
5617 # scope limited to repo, which requester have access to
5612 scope_repo_id = Column(
5618 scope_repo_id = Column(
5613 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5619 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5614 nullable=True, unique=None, default=None)
5620 nullable=True, unique=None, default=None)
5615 repo = relationship('Repository', lazy='joined')
5621 repo = relationship('Repository', lazy='joined')
5616
5622
5617 # scope limited to repo group, which requester have access to
5623 # scope limited to repo group, which requester have access to
5618 scope_repo_group_id = Column(
5624 scope_repo_group_id = Column(
5619 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5625 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5620 nullable=True, unique=None, default=None)
5626 nullable=True, unique=None, default=None)
5621 repo_group = relationship('RepoGroup', lazy='joined')
5627 repo_group = relationship('RepoGroup', lazy='joined')
5622
5628
5623 @classmethod
5629 @classmethod
5624 def get_scope(cls, scope_type, scope_id):
5630 def get_scope(cls, scope_type, scope_id):
5625 if scope_type == 'repo':
5631 if scope_type == 'repo':
5626 return f'repo:{scope_id}'
5632 return f'repo:{scope_id}'
5627 elif scope_type == 'repo-group':
5633 elif scope_type == 'repo-group':
5628 return f'repo-group:{scope_id}'
5634 return f'repo-group:{scope_id}'
5629 elif scope_type == 'user':
5635 elif scope_type == 'user':
5630 return f'user:{scope_id}'
5636 return f'user:{scope_id}'
5631 elif scope_type == 'user-group':
5637 elif scope_type == 'user-group':
5632 return f'user-group:{scope_id}'
5638 return f'user-group:{scope_id}'
5633 else:
5639 else:
5634 return scope_type
5640 return scope_type
5635
5641
5636 @classmethod
5642 @classmethod
5637 def get_by_store_uid(cls, file_store_uid, safe=False):
5643 def get_by_store_uid(cls, file_store_uid, safe=False):
5638 if safe:
5644 if safe:
5639 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5645 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5640 else:
5646 else:
5641 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5647 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5642
5648
5643 @classmethod
5649 @classmethod
5644 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5650 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5645 file_description='', enabled=True, hidden=False, check_acl=True,
5651 file_description='', enabled=True, hidden=False, check_acl=True,
5646 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5652 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5647
5653
5648 store_entry = FileStore()
5654 store_entry = FileStore()
5649 store_entry.file_uid = file_uid
5655 store_entry.file_uid = file_uid
5650 store_entry.file_display_name = file_display_name
5656 store_entry.file_display_name = file_display_name
5651 store_entry.file_org_name = filename
5657 store_entry.file_org_name = filename
5652 store_entry.file_size = file_size
5658 store_entry.file_size = file_size
5653 store_entry.file_hash = file_hash
5659 store_entry.file_hash = file_hash
5654 store_entry.file_description = file_description
5660 store_entry.file_description = file_description
5655
5661
5656 store_entry.check_acl = check_acl
5662 store_entry.check_acl = check_acl
5657 store_entry.enabled = enabled
5663 store_entry.enabled = enabled
5658 store_entry.hidden = hidden
5664 store_entry.hidden = hidden
5659
5665
5660 store_entry.user_id = user_id
5666 store_entry.user_id = user_id
5661 store_entry.scope_user_id = scope_user_id
5667 store_entry.scope_user_id = scope_user_id
5662 store_entry.scope_repo_id = scope_repo_id
5668 store_entry.scope_repo_id = scope_repo_id
5663 store_entry.scope_repo_group_id = scope_repo_group_id
5669 store_entry.scope_repo_group_id = scope_repo_group_id
5664
5670
5665 return store_entry
5671 return store_entry
5666
5672
5667 @classmethod
5673 @classmethod
5668 def store_metadata(cls, file_store_id, args, commit=True):
5674 def store_metadata(cls, file_store_id, args, commit=True):
5669 file_store = FileStore.get(file_store_id)
5675 file_store = FileStore.get(file_store_id)
5670 if file_store is None:
5676 if file_store is None:
5671 return
5677 return
5672
5678
5673 for section, key, value, value_type in args:
5679 for section, key, value, value_type in args:
5674 has_key = FileStoreMetadata().query() \
5680 has_key = FileStoreMetadata().query() \
5675 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5681 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5676 .filter(FileStoreMetadata.file_store_meta_section == section) \
5682 .filter(FileStoreMetadata.file_store_meta_section == section) \
5677 .filter(FileStoreMetadata.file_store_meta_key == key) \
5683 .filter(FileStoreMetadata.file_store_meta_key == key) \
5678 .scalar()
5684 .scalar()
5679 if has_key:
5685 if has_key:
5680 msg = 'key `{}` already defined under section `{}` for this file.'\
5686 msg = 'key `{}` already defined under section `{}` for this file.'\
5681 .format(key, section)
5687 .format(key, section)
5682 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5688 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5683
5689
5684 # NOTE(marcink): raises ArtifactMetadataBadValueType
5690 # NOTE(marcink): raises ArtifactMetadataBadValueType
5685 FileStoreMetadata.valid_value_type(value_type)
5691 FileStoreMetadata.valid_value_type(value_type)
5686
5692
5687 meta_entry = FileStoreMetadata()
5693 meta_entry = FileStoreMetadata()
5688 meta_entry.file_store = file_store
5694 meta_entry.file_store = file_store
5689 meta_entry.file_store_meta_section = section
5695 meta_entry.file_store_meta_section = section
5690 meta_entry.file_store_meta_key = key
5696 meta_entry.file_store_meta_key = key
5691 meta_entry.file_store_meta_value_type = value_type
5697 meta_entry.file_store_meta_value_type = value_type
5692 meta_entry.file_store_meta_value = value
5698 meta_entry.file_store_meta_value = value
5693
5699
5694 Session().add(meta_entry)
5700 Session().add(meta_entry)
5695
5701
5696 try:
5702 try:
5697 if commit:
5703 if commit:
5698 Session().commit()
5704 Session().commit()
5699 except IntegrityError:
5705 except IntegrityError:
5700 Session().rollback()
5706 Session().rollback()
5701 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5707 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5702
5708
5703 @classmethod
5709 @classmethod
5704 def bump_access_counter(cls, file_uid, commit=True):
5710 def bump_access_counter(cls, file_uid, commit=True):
5705 FileStore().query()\
5711 FileStore().query()\
5706 .filter(FileStore.file_uid == file_uid)\
5712 .filter(FileStore.file_uid == file_uid)\
5707 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5713 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5708 FileStore.accessed_on: datetime.datetime.now()})
5714 FileStore.accessed_on: datetime.datetime.now()})
5709 if commit:
5715 if commit:
5710 Session().commit()
5716 Session().commit()
5711
5717
5712 def __json__(self):
5718 def __json__(self):
5713 data = {
5719 data = {
5714 'filename': self.file_display_name,
5720 'filename': self.file_display_name,
5715 'filename_org': self.file_org_name,
5721 'filename_org': self.file_org_name,
5716 'file_uid': self.file_uid,
5722 'file_uid': self.file_uid,
5717 'description': self.file_description,
5723 'description': self.file_description,
5718 'hidden': self.hidden,
5724 'hidden': self.hidden,
5719 'size': self.file_size,
5725 'size': self.file_size,
5720 'created_on': self.created_on,
5726 'created_on': self.created_on,
5721 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5727 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5722 'downloaded_times': self.accessed_count,
5728 'downloaded_times': self.accessed_count,
5723 'sha256': self.file_hash,
5729 'sha256': self.file_hash,
5724 'metadata': self.file_metadata,
5730 'metadata': self.file_metadata,
5725 }
5731 }
5726
5732
5727 return data
5733 return data
5728
5734
5729 def __repr__(self):
5735 def __repr__(self):
5730 return f'<FileStore({self.file_store_id})>'
5736 return f'<FileStore({self.file_store_id})>'
5731
5737
5732
5738
5733 class FileStoreMetadata(Base, BaseModel):
5739 class FileStoreMetadata(Base, BaseModel):
5734 __tablename__ = 'file_store_metadata'
5740 __tablename__ = 'file_store_metadata'
5735 __table_args__ = (
5741 __table_args__ = (
5736 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5742 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5737 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5743 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5738 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5744 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5739 base_table_args
5745 base_table_args
5740 )
5746 )
5741 SETTINGS_TYPES = {
5747 SETTINGS_TYPES = {
5742 'str': safe_str,
5748 'str': safe_str,
5743 'int': safe_int,
5749 'int': safe_int,
5744 'unicode': safe_str,
5750 'unicode': safe_str,
5745 'bool': str2bool,
5751 'bool': str2bool,
5746 'list': functools.partial(aslist, sep=',')
5752 'list': functools.partial(aslist, sep=',')
5747 }
5753 }
5748
5754
5749 file_store_meta_id = Column(
5755 file_store_meta_id = Column(
5750 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5756 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5751 primary_key=True)
5757 primary_key=True)
5752 _file_store_meta_section = Column(
5758 _file_store_meta_section = Column(
5753 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5759 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5754 nullable=True, unique=None, default=None)
5760 nullable=True, unique=None, default=None)
5755 _file_store_meta_section_hash = Column(
5761 _file_store_meta_section_hash = Column(
5756 "file_store_meta_section_hash", String(255),
5762 "file_store_meta_section_hash", String(255),
5757 nullable=True, unique=None, default=None)
5763 nullable=True, unique=None, default=None)
5758 _file_store_meta_key = Column(
5764 _file_store_meta_key = Column(
5759 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5765 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5760 nullable=True, unique=None, default=None)
5766 nullable=True, unique=None, default=None)
5761 _file_store_meta_key_hash = Column(
5767 _file_store_meta_key_hash = Column(
5762 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5768 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5763 _file_store_meta_value = Column(
5769 _file_store_meta_value = Column(
5764 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5770 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5765 nullable=True, unique=None, default=None)
5771 nullable=True, unique=None, default=None)
5766 _file_store_meta_value_type = Column(
5772 _file_store_meta_value_type = Column(
5767 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5773 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5768 default='unicode')
5774 default='unicode')
5769
5775
5770 file_store_id = Column(
5776 file_store_id = Column(
5771 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5777 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5772 nullable=True, unique=None, default=None)
5778 nullable=True, unique=None, default=None)
5773
5779
5774 file_store = relationship('FileStore', lazy='joined', viewonly=True)
5780 file_store = relationship('FileStore', lazy='joined', viewonly=True)
5775
5781
5776 @classmethod
5782 @classmethod
5777 def valid_value_type(cls, value):
5783 def valid_value_type(cls, value):
5778 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5784 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5779 raise ArtifactMetadataBadValueType(
5785 raise ArtifactMetadataBadValueType(
5780 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5786 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5781
5787
5782 @hybrid_property
5788 @hybrid_property
5783 def file_store_meta_section(self):
5789 def file_store_meta_section(self):
5784 return self._file_store_meta_section
5790 return self._file_store_meta_section
5785
5791
5786 @file_store_meta_section.setter
5792 @file_store_meta_section.setter
5787 def file_store_meta_section(self, value):
5793 def file_store_meta_section(self, value):
5788 self._file_store_meta_section = value
5794 self._file_store_meta_section = value
5789 self._file_store_meta_section_hash = _hash_key(value)
5795 self._file_store_meta_section_hash = _hash_key(value)
5790
5796
5791 @hybrid_property
5797 @hybrid_property
5792 def file_store_meta_key(self):
5798 def file_store_meta_key(self):
5793 return self._file_store_meta_key
5799 return self._file_store_meta_key
5794
5800
5795 @file_store_meta_key.setter
5801 @file_store_meta_key.setter
5796 def file_store_meta_key(self, value):
5802 def file_store_meta_key(self, value):
5797 self._file_store_meta_key = value
5803 self._file_store_meta_key = value
5798 self._file_store_meta_key_hash = _hash_key(value)
5804 self._file_store_meta_key_hash = _hash_key(value)
5799
5805
5800 @hybrid_property
5806 @hybrid_property
5801 def file_store_meta_value(self):
5807 def file_store_meta_value(self):
5802 val = self._file_store_meta_value
5808 val = self._file_store_meta_value
5803
5809
5804 if self._file_store_meta_value_type:
5810 if self._file_store_meta_value_type:
5805 # e.g unicode.encrypted == unicode
5811 # e.g unicode.encrypted == unicode
5806 _type = self._file_store_meta_value_type.split('.')[0]
5812 _type = self._file_store_meta_value_type.split('.')[0]
5807 # decode the encrypted value if it's encrypted field type
5813 # decode the encrypted value if it's encrypted field type
5808 if '.encrypted' in self._file_store_meta_value_type:
5814 if '.encrypted' in self._file_store_meta_value_type:
5809 cipher = EncryptedTextValue()
5815 cipher = EncryptedTextValue()
5810 val = safe_str(cipher.process_result_value(val, None))
5816 val = safe_str(cipher.process_result_value(val, None))
5811 # do final type conversion
5817 # do final type conversion
5812 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5818 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5813 val = converter(val)
5819 val = converter(val)
5814
5820
5815 return val
5821 return val
5816
5822
5817 @file_store_meta_value.setter
5823 @file_store_meta_value.setter
5818 def file_store_meta_value(self, val):
5824 def file_store_meta_value(self, val):
5819 val = safe_str(val)
5825 val = safe_str(val)
5820 # encode the encrypted value
5826 # encode the encrypted value
5821 if '.encrypted' in self.file_store_meta_value_type:
5827 if '.encrypted' in self.file_store_meta_value_type:
5822 cipher = EncryptedTextValue()
5828 cipher = EncryptedTextValue()
5823 val = safe_str(cipher.process_bind_param(val, None))
5829 val = safe_str(cipher.process_bind_param(val, None))
5824 self._file_store_meta_value = val
5830 self._file_store_meta_value = val
5825
5831
5826 @hybrid_property
5832 @hybrid_property
5827 def file_store_meta_value_type(self):
5833 def file_store_meta_value_type(self):
5828 return self._file_store_meta_value_type
5834 return self._file_store_meta_value_type
5829
5835
5830 @file_store_meta_value_type.setter
5836 @file_store_meta_value_type.setter
5831 def file_store_meta_value_type(self, val):
5837 def file_store_meta_value_type(self, val):
5832 # e.g unicode.encrypted
5838 # e.g unicode.encrypted
5833 self.valid_value_type(val)
5839 self.valid_value_type(val)
5834 self._file_store_meta_value_type = val
5840 self._file_store_meta_value_type = val
5835
5841
5836 def __json__(self):
5842 def __json__(self):
5837 data = {
5843 data = {
5838 'artifact': self.file_store.file_uid,
5844 'artifact': self.file_store.file_uid,
5839 'section': self.file_store_meta_section,
5845 'section': self.file_store_meta_section,
5840 'key': self.file_store_meta_key,
5846 'key': self.file_store_meta_key,
5841 'value': self.file_store_meta_value,
5847 'value': self.file_store_meta_value,
5842 }
5848 }
5843
5849
5844 return data
5850 return data
5845
5851
5846 def __repr__(self):
5852 def __repr__(self):
5847 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.file_store_meta_section,
5853 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.file_store_meta_section,
5848 self.file_store_meta_key, self.file_store_meta_value)
5854 self.file_store_meta_key, self.file_store_meta_value)
5849
5855
5850
5856
5851 class DbMigrateVersion(Base, BaseModel):
5857 class DbMigrateVersion(Base, BaseModel):
5852 __tablename__ = 'db_migrate_version'
5858 __tablename__ = 'db_migrate_version'
5853 __table_args__ = (
5859 __table_args__ = (
5854 base_table_args,
5860 base_table_args,
5855 )
5861 )
5856
5862
5857 repository_id = Column('repository_id', String(250), primary_key=True)
5863 repository_id = Column('repository_id', String(250), primary_key=True)
5858 repository_path = Column('repository_path', Text)
5864 repository_path = Column('repository_path', Text)
5859 version = Column('version', Integer)
5865 version = Column('version', Integer)
5860
5866
5861 @classmethod
5867 @classmethod
5862 def set_version(cls, version):
5868 def set_version(cls, version):
5863 """
5869 """
5864 Helper for forcing a different version, usually for debugging purposes via ishell.
5870 Helper for forcing a different version, usually for debugging purposes via ishell.
5865 """
5871 """
5866 ver = DbMigrateVersion.query().first()
5872 ver = DbMigrateVersion.query().first()
5867 ver.version = version
5873 ver.version = version
5868 Session().commit()
5874 Session().commit()
5869
5875
5870
5876
5871 class DbSession(Base, BaseModel):
5877 class DbSession(Base, BaseModel):
5872 __tablename__ = 'db_session'
5878 __tablename__ = 'db_session'
5873 __table_args__ = (
5879 __table_args__ = (
5874 base_table_args,
5880 base_table_args,
5875 )
5881 )
5876
5882
5877 def __repr__(self):
5883 def __repr__(self):
5878 return f'<DB:DbSession({self.id})>'
5884 return f'<DB:DbSession({self.id})>'
5879
5885
5880 id = Column('id', Integer())
5886 id = Column('id', Integer())
5881 namespace = Column('namespace', String(255), primary_key=True)
5887 namespace = Column('namespace', String(255), primary_key=True)
5882 accessed = Column('accessed', DateTime, nullable=False)
5888 accessed = Column('accessed', DateTime, nullable=False)
5883 created = Column('created', DateTime, nullable=False)
5889 created = Column('created', DateTime, nullable=False)
5884 data = Column('data', PickleType, nullable=False)
5890 data = Column('data', PickleType, nullable=False)
@@ -1,1111 +1,1111 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 """
19 """
20 Set of generic validators
20 Set of generic validators
21 """
21 """
22
22
23
23
24 import os
24 import os
25 import re
25 import re
26 import logging
26 import logging
27 import collections
27 import collections
28
28
29 import formencode
29 import formencode
30 import ipaddress
30 import ipaddress
31 from formencode.validators import (
31 from formencode.validators import (
32 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set,
32 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set,
33 NotEmpty, IPAddress, CIDR, String, FancyValidator
33 NotEmpty, IPAddress, CIDR, String, FancyValidator
34 )
34 )
35
35
36 from sqlalchemy.sql.expression import true
36 from sqlalchemy.sql.expression import true
37 from sqlalchemy.util import OrderedSet
37 from sqlalchemy.util import OrderedSet
38
38
39 from rhodecode.authentication import (
39 from rhodecode.authentication import (
40 legacy_plugin_prefix, _import_legacy_plugin)
40 legacy_plugin_prefix, _import_legacy_plugin)
41 from rhodecode.authentication.base import loadplugin
41 from rhodecode.authentication.base import loadplugin
42 from rhodecode.apps._base import ADMIN_PREFIX
42 from rhodecode.apps._base import ADMIN_PREFIX
43 from rhodecode.lib.auth import HasRepoGroupPermissionAny, HasPermissionAny
43 from rhodecode.lib.auth import HasRepoGroupPermissionAny, HasPermissionAny
44 from rhodecode.lib.utils import repo_name_slug, make_db_config
44 from rhodecode.lib.utils import repo_name_slug, make_db_config
45 from rhodecode.lib.utils2 import safe_int, str2bool, aslist
45 from rhodecode.lib.utils2 import safe_int, str2bool, aslist
46 from rhodecode.lib.str_utils import safe_str
46 from rhodecode.lib.str_utils import safe_str
47 from rhodecode.lib.hash_utils import md5_safe
47 from rhodecode.lib.hash_utils import md5_safe
48 from rhodecode.lib.vcs.backends.git.repository import GitRepository
48 from rhodecode.lib.vcs.backends.git.repository import GitRepository
49 from rhodecode.lib.vcs.backends.hg.repository import MercurialRepository
49 from rhodecode.lib.vcs.backends.hg.repository import MercurialRepository
50 from rhodecode.lib.vcs.backends.svn.repository import SubversionRepository
50 from rhodecode.lib.vcs.backends.svn.repository import SubversionRepository
51 from rhodecode.model.db import (
51 from rhodecode.model.db import (
52 RepoGroup, Repository, UserGroup, User, ChangesetStatus, Gist)
52 RepoGroup, Repository, UserGroup, User, ChangesetStatus, Gist)
53 from rhodecode.model.settings import VcsSettingsModel
53 from rhodecode.model.settings import VcsSettingsModel
54
54
55 # silence warnings and pylint
55 # silence warnings and pylint
56 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \
56 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \
57 NotEmpty, IPAddress, CIDR, String, FancyValidator
57 NotEmpty, IPAddress, CIDR, String, FancyValidator
58
58
59 log = logging.getLogger(__name__)
59 log = logging.getLogger(__name__)
60
60
61
61
62 class _Missing(object):
62 class _Missing(object):
63 pass
63 pass
64
64
65
65
66 Missing = _Missing()
66 Missing = _Missing()
67
67
68
68
69 def M(self, key, state, **kwargs):
69 def M(self, key, state, **kwargs):
70 """
70 """
71 returns string from self.message based on given key,
71 returns string from self.message based on given key,
72 passed kw params are used to substitute %(named)s params inside
72 passed kw params are used to substitute %(named)s params inside
73 translated strings
73 translated strings
74
74
75 :param msg:
75 :param msg:
76 :param state:
76 :param state:
77 """
77 """
78
78
79 #state._ = staticmethod(_)
79 #state._ = staticmethod(_)
80 # inject validator into state object
80 # inject validator into state object
81 return self.message(key, state, **kwargs)
81 return self.message(key, state, **kwargs)
82
82
83
83
84 def UniqueList(localizer, convert=None):
84 def UniqueList(localizer, convert=None):
85 _ = localizer
85 _ = localizer
86
86
87 class _validator(formencode.FancyValidator):
87 class _validator(formencode.FancyValidator):
88 """
88 """
89 Unique List !
89 Unique List !
90 """
90 """
91 accept_iterator = True
91 accept_iterator = True
92
92
93 messages = {
93 messages = {
94 'empty': _('Value cannot be an empty list'),
94 'empty': _('Value cannot be an empty list'),
95 'missing_value': _('Value cannot be an empty list'),
95 'missing_value': _('Value cannot be an empty list'),
96 }
96 }
97
97
98 def _convert_to_python(self, value, state):
98 def _convert_to_python(self, value, state):
99
99
100 def make_unique(_value):
100 def make_unique(_value):
101 seen = []
101 seen = []
102 return [c for c in _value if not (c in seen or seen.append(c))]
102 return [c for c in _value if not (c in seen or seen.append(c))]
103
103
104 if isinstance(value, list):
104 if isinstance(value, list):
105 ret_val = make_unique(value)
105 ret_val = make_unique(value)
106 elif isinstance(value, set):
106 elif isinstance(value, set):
107 ret_val = make_unique(list(value))
107 ret_val = make_unique(list(value))
108 elif isinstance(value, tuple):
108 elif isinstance(value, tuple):
109 ret_val = make_unique(list(value))
109 ret_val = make_unique(list(value))
110 elif value is None:
110 elif value is None:
111 ret_val = []
111 ret_val = []
112 else:
112 else:
113 ret_val = [value]
113 ret_val = [value]
114
114
115 if convert:
115 if convert:
116 ret_val = list(map(convert, ret_val))
116 ret_val = list(map(convert, ret_val))
117 return ret_val
117 return ret_val
118
118
119 def empty_value(self, value):
119 def empty_value(self, value):
120 return []
120 return []
121
121
122 return _validator
122 return _validator
123
123
124
124
125 def UniqueListFromString(localizer):
125 def UniqueListFromString(localizer):
126 _ = localizer
126 _ = localizer
127
127
128 class _validator(UniqueList(localizer)):
128 class _validator(UniqueList(localizer)):
129 def _convert_to_python(self, value, state):
129 def _convert_to_python(self, value, state):
130 if isinstance(value, str):
130 if isinstance(value, str):
131 value = aslist(value, ',')
131 value = aslist(value, ',')
132 return super()._convert_to_python(value, state)
132 return super()._convert_to_python(value, state)
133 return _validator
133 return _validator
134
134
135
135
136 def ValidSvnPattern(localizer, section, repo_name=None):
136 def ValidSvnPattern(localizer, section, repo_name=None):
137 _ = localizer
137 _ = localizer
138
138
139 class _validator(formencode.validators.FancyValidator):
139 class _validator(formencode.validators.FancyValidator):
140 messages = {
140 messages = {
141 'pattern_exists': _('Pattern already exists'),
141 'pattern_exists': _('Pattern already exists'),
142 }
142 }
143
143
144 def _validate_python(self, value, state):
144 def _validate_python(self, value, state):
145 if not value:
145 if not value:
146 return
146 return
147 model = VcsSettingsModel(repo=repo_name)
147 model = VcsSettingsModel(repo=repo_name)
148 ui_settings = model.get_svn_patterns(section=section)
148 ui_settings = model.get_svn_patterns(section=section)
149 for entry in ui_settings:
149 for entry in ui_settings:
150 if value == entry.value:
150 if value == entry.value:
151 msg = M(self, 'pattern_exists', state)
151 msg = M(self, 'pattern_exists', state)
152 raise formencode.Invalid(msg, value, state)
152 raise formencode.Invalid(msg, value, state)
153 return _validator
153 return _validator
154
154
155
155
156 def ValidUsername(localizer, edit=False, old_data=None):
156 def ValidUsername(localizer, edit=False, old_data=None):
157 _ = localizer
157 _ = localizer
158 old_data = old_data or {}
158 old_data = old_data or {}
159
159
160 class _validator(formencode.validators.FancyValidator):
160 class _validator(formencode.validators.FancyValidator):
161 messages = {
161 messages = {
162 'username_exists': _('Username "%(username)s" already exists'),
162 'username_exists': _('Username "%(username)s" already exists'),
163 'system_invalid_username':
163 'system_invalid_username':
164 _('Username "%(username)s" is forbidden'),
164 _('Username "%(username)s" is forbidden'),
165 'invalid_username':
165 'invalid_username':
166 _('Username may only contain alphanumeric characters '
166 _('Username may only contain alphanumeric characters '
167 'underscores, periods or dashes and must begin with '
167 'underscores, periods or dashes and must begin with '
168 'alphanumeric character or underscore')
168 'alphanumeric character or underscore')
169 }
169 }
170
170
171 def _validate_python(self, value, state):
171 def _validate_python(self, value, state):
172 if value in ['default', 'new_user']:
172 if value in ['default', 'new_user']:
173 msg = M(self, 'system_invalid_username', state, username=value)
173 msg = M(self, 'system_invalid_username', state, username=value)
174 raise formencode.Invalid(msg, value, state)
174 raise formencode.Invalid(msg, value, state)
175 # check if user is unique
175 # check if user is unique
176 old_un = None
176 old_un = None
177 if edit:
177 if edit:
178 old_un = User.get(old_data.get('user_id')).username
178 old_un = User.get(old_data.get('user_id')).username
179
179
180 if old_un != value or not edit:
180 if old_un != value or not edit:
181 if User.get_by_username(value, case_insensitive=True):
181 if User.get_by_username(value, case_insensitive=True):
182 msg = M(self, 'username_exists', state, username=value)
182 msg = M(self, 'username_exists', state, username=value)
183 raise formencode.Invalid(msg, value, state)
183 raise formencode.Invalid(msg, value, state)
184
184
185 if (re.match(r'^[\w]{1}[\w\-\.]{0,254}$', value)
185 if (re.match(r'^[\w]{1}[\w\-\.]{0,254}$', value)
186 is None):
186 is None):
187 msg = M(self, 'invalid_username', state)
187 msg = M(self, 'invalid_username', state)
188 raise formencode.Invalid(msg, value, state)
188 raise formencode.Invalid(msg, value, state)
189 return _validator
189 return _validator
190
190
191
191
192 def ValidRepoUser(localizer, allow_disabled=False):
192 def ValidRepoUser(localizer, allow_disabled=False):
193 _ = localizer
193 _ = localizer
194
194
195 class _validator(formencode.validators.FancyValidator):
195 class _validator(formencode.validators.FancyValidator):
196 messages = {
196 messages = {
197 'invalid_username': _('Username %(username)s is not valid'),
197 'invalid_username': _('Username %(username)s is not valid'),
198 'disabled_username': _('Username %(username)s is disabled')
198 'disabled_username': _('Username %(username)s is disabled')
199 }
199 }
200
200
201 def _validate_python(self, value, state):
201 def _validate_python(self, value, state):
202 try:
202 try:
203 user = User.query().filter(User.username == value).one()
203 user = User.query().filter(User.username == value).one()
204 except Exception:
204 except Exception:
205 msg = M(self, 'invalid_username', state, username=value)
205 msg = M(self, 'invalid_username', state, username=value)
206 raise formencode.Invalid(
206 raise formencode.Invalid(
207 msg, value, state, error_dict={'username': msg}
207 msg, value, state, error_dict={'username': msg}
208 )
208 )
209 if user and (not allow_disabled and not user.active):
209 if user and (not allow_disabled and not user.active):
210 msg = M(self, 'disabled_username', state, username=value)
210 msg = M(self, 'disabled_username', state, username=value)
211 raise formencode.Invalid(
211 raise formencode.Invalid(
212 msg, value, state, error_dict={'username': msg}
212 msg, value, state, error_dict={'username': msg}
213 )
213 )
214 return _validator
214 return _validator
215
215
216
216
217 def ValidUserGroup(localizer, edit=False, old_data=None):
217 def ValidUserGroup(localizer, edit=False, old_data=None):
218 _ = localizer
218 _ = localizer
219 old_data = old_data or {}
219 old_data = old_data or {}
220
220
221 class _validator(formencode.validators.FancyValidator):
221 class _validator(formencode.validators.FancyValidator):
222 messages = {
222 messages = {
223 'invalid_group': _('Invalid user group name'),
223 'invalid_group': _('Invalid user group name'),
224 'group_exist': _('User group `%(usergroup)s` already exists'),
224 'group_exist': _('User group `%(usergroup)s` already exists'),
225 'invalid_usergroup_name':
225 'invalid_usergroup_name':
226 _('User group name may only contain alphanumeric '
226 _('User group name may only contain alphanumeric '
227 'characters underscores, periods or dashes and must begin '
227 'characters underscores, periods or dashes and must begin '
228 'with alphanumeric character')
228 'with alphanumeric character')
229 }
229 }
230
230
231 def _validate_python(self, value, state):
231 def _validate_python(self, value, state):
232 if value in ['default']:
232 if value in ['default']:
233 msg = M(self, 'invalid_group', state)
233 msg = M(self, 'invalid_group', state)
234 raise formencode.Invalid(msg, value, state)
234 raise formencode.Invalid(msg, value, state)
235 # check if group is unique
235 # check if group is unique
236 old_ugname = None
236 old_ugname = None
237 if edit:
237 if edit:
238 old_id = old_data.get('users_group_id')
238 old_id = old_data.get('users_group_id')
239 old_ugname = UserGroup.get(old_id).users_group_name
239 old_ugname = UserGroup.get(old_id).users_group_name
240
240
241 if old_ugname != value or not edit:
241 if old_ugname != value or not edit:
242 is_existing_group = UserGroup.get_by_group_name(
242 is_existing_group = UserGroup.get_by_group_name(
243 value, case_insensitive=True)
243 value, case_insensitive=True)
244 if is_existing_group:
244 if is_existing_group:
245 msg = M(self, 'group_exist', state, usergroup=value)
245 msg = M(self, 'group_exist', state, usergroup=value)
246 raise formencode.Invalid(
246 raise formencode.Invalid(
247 msg, value, state, error_dict={'users_group_name': msg}
247 msg, value, state, error_dict={'users_group_name': msg}
248 )
248 )
249
249
250 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
250 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
251 msg = M(self, 'invalid_usergroup_name', state)
251 msg = M(self, 'invalid_usergroup_name', state)
252 raise formencode.Invalid(msg, value, state)
252 raise formencode.Invalid(msg, value, state)
253 return _validator
253 return _validator
254
254
255
255
256 def ValidRepoGroup(localizer, edit=False, old_data=None, can_create_in_root=False):
256 def ValidRepoGroup(localizer, edit=False, old_data=None, can_create_in_root=False):
257 _ = localizer
257 _ = localizer
258 old_data = old_data or {}
258 old_data = old_data or {}
259
259
260 class _validator(formencode.validators.FancyValidator):
260 class _validator(formencode.validators.FancyValidator):
261 messages = {
261 messages = {
262 'group_parent_id': _('Cannot assign this group as parent'),
262 'group_parent_id': _('Cannot assign this group as parent'),
263 'group_exists': _('Group "%(group_name)s" already exists'),
263 'group_exists': _('Group "%(group_name)s" already exists'),
264 'repo_exists': _('Repository with name "%(group_name)s" '
264 'repo_exists': _('Repository with name "%(group_name)s" '
265 'already exists'),
265 'already exists'),
266 'permission_denied': _("no permission to store repository group"
266 'permission_denied': _("no permission to store repository group"
267 "in this location"),
267 "in this location"),
268 'permission_denied_root': _(
268 'permission_denied_root': _(
269 "no permission to store repository group "
269 "no permission to store repository group "
270 "in root location")
270 "in root location")
271 }
271 }
272
272
273 def _convert_to_python(self, value, state):
273 def _convert_to_python(self, value, state):
274 group_name = repo_name_slug(value.get('group_name', ''))
274 group_name = repo_name_slug(value.get('group_name', ''))
275 group_parent_id = safe_int(value.get('group_parent_id'))
275 group_parent_id = safe_int(value.get('group_parent_id'))
276 gr = RepoGroup.get(group_parent_id)
276 gr = RepoGroup.get(group_parent_id)
277 if gr:
277 if gr:
278 parent_group_path = gr.full_path
278 parent_group_path = gr.full_path
279 # value needs to be aware of group name in order to check
279 # value needs to be aware of group name in order to check
280 # db key This is an actual just the name to store in the
280 # db key This is an actual just the name to store in the
281 # database
281 # database
282 group_name_full = (
282 group_name_full = (
283 parent_group_path + RepoGroup.url_sep() + group_name)
283 parent_group_path + RepoGroup.url_sep() + group_name)
284 else:
284 else:
285 group_name_full = group_name
285 group_name_full = group_name
286
286
287 value['group_name'] = group_name
287 value['group_name'] = group_name
288 value['group_name_full'] = group_name_full
288 value['group_name_full'] = group_name_full
289 value['group_parent_id'] = group_parent_id
289 value['group_parent_id'] = group_parent_id
290 return value
290 return value
291
291
292 def _validate_python(self, value, state):
292 def _validate_python(self, value, state):
293
293
294 old_group_name = None
294 old_group_name = None
295 group_name = value.get('group_name')
295 group_name = value.get('group_name')
296 group_name_full = value.get('group_name_full')
296 group_name_full = value.get('group_name_full')
297 group_parent_id = safe_int(value.get('group_parent_id'))
297 group_parent_id = safe_int(value.get('group_parent_id'))
298 if group_parent_id == -1:
298 if group_parent_id == -1:
299 group_parent_id = None
299 group_parent_id = None
300
300
301 group_obj = RepoGroup.get(old_data.get('group_id'))
301 group_obj = RepoGroup.get(old_data.get('group_id'))
302 parent_group_changed = False
302 parent_group_changed = False
303 if edit:
303 if edit:
304 old_group_name = group_obj.group_name
304 old_group_name = group_obj.group_name
305 old_group_parent_id = group_obj.group_parent_id
305 old_group_parent_id = group_obj.group_parent_id
306
306
307 if group_parent_id != old_group_parent_id:
307 if group_parent_id != old_group_parent_id:
308 parent_group_changed = True
308 parent_group_changed = True
309
309
310 # TODO: mikhail: the following if statement is not reached
310 # TODO: mikhail: the following if statement is not reached
311 # since group_parent_id's OneOf validation fails before.
311 # since group_parent_id's OneOf validation fails before.
312 # Can be removed.
312 # Can be removed.
313
313
314 # check against setting a parent of self
314 # check against setting a parent of self
315 parent_of_self = (
315 parent_of_self = (
316 old_data['group_id'] == group_parent_id
316 old_data['group_id'] == group_parent_id
317 if group_parent_id else False
317 if group_parent_id else False
318 )
318 )
319 if parent_of_self:
319 if parent_of_self:
320 msg = M(self, 'group_parent_id', state)
320 msg = M(self, 'group_parent_id', state)
321 raise formencode.Invalid(
321 raise formencode.Invalid(
322 msg, value, state, error_dict={'group_parent_id': msg}
322 msg, value, state, error_dict={'group_parent_id': msg}
323 )
323 )
324
324
325 # group we're moving current group inside
325 # group we're moving current group inside
326 child_group = None
326 child_group = None
327 if group_parent_id:
327 if group_parent_id:
328 child_group = RepoGroup.query().filter(
328 child_group = RepoGroup.query().filter(
329 RepoGroup.group_id == group_parent_id).scalar()
329 RepoGroup.group_id == group_parent_id).scalar()
330
330
331 # do a special check that we cannot move a group to one of
331 # do a special check that we cannot move a group to one of
332 # it's children
332 # it's children
333 if edit and child_group:
333 if edit and child_group:
334 parents = [x.group_id for x in child_group.parents]
334 parents = [x.group_id for x in child_group.parents]
335 move_to_children = old_data['group_id'] in parents
335 move_to_children = old_data['group_id'] in parents
336 if move_to_children:
336 if move_to_children:
337 msg = M(self, 'group_parent_id', state)
337 msg = M(self, 'group_parent_id', state)
338 raise formencode.Invalid(
338 raise formencode.Invalid(
339 msg, value, state, error_dict={'group_parent_id': msg})
339 msg, value, state, error_dict={'group_parent_id': msg})
340
340
341 # Check if we have permission to store in the parent.
341 # Check if we have permission to store in the parent.
342 # Only check if the parent group changed.
342 # Only check if the parent group changed.
343 if parent_group_changed:
343 if parent_group_changed:
344 if child_group is None:
344 if child_group is None:
345 if not can_create_in_root:
345 if not can_create_in_root:
346 msg = M(self, 'permission_denied_root', state)
346 msg = M(self, 'permission_denied_root', state)
347 raise formencode.Invalid(
347 raise formencode.Invalid(
348 msg, value, state,
348 msg, value, state,
349 error_dict={'group_parent_id': msg})
349 error_dict={'group_parent_id': msg})
350 else:
350 else:
351 valid = HasRepoGroupPermissionAny('group.admin')
351 valid = HasRepoGroupPermissionAny('group.admin')
352 forbidden = not valid(
352 forbidden = not valid(
353 child_group.group_name, 'can create group validator')
353 child_group.group_name, 'can create group validator')
354 if forbidden:
354 if forbidden:
355 msg = M(self, 'permission_denied', state)
355 msg = M(self, 'permission_denied', state)
356 raise formencode.Invalid(
356 raise formencode.Invalid(
357 msg, value, state,
357 msg, value, state,
358 error_dict={'group_parent_id': msg})
358 error_dict={'group_parent_id': msg})
359
359
360 # if we change the name or it's new group, check for existing names
360 # if we change the name or it's new group, check for existing names
361 # or repositories with the same name
361 # or repositories with the same name
362 if old_group_name != group_name_full or not edit:
362 if old_group_name != group_name_full or not edit:
363 # check group
363 # check group
364 gr = RepoGroup.get_by_group_name(group_name_full)
364 gr = RepoGroup.get_by_group_name(group_name_full)
365 if gr:
365 if gr:
366 msg = M(self, 'group_exists', state, group_name=group_name)
366 msg = M(self, 'group_exists', state, group_name=group_name)
367 raise formencode.Invalid(
367 raise formencode.Invalid(
368 msg, value, state, error_dict={'group_name': msg})
368 msg, value, state, error_dict={'group_name': msg})
369
369
370 # check for same repo
370 # check for same repo
371 repo = Repository.get_by_repo_name(group_name_full)
371 repo = Repository.get_by_repo_name(group_name_full)
372 if repo:
372 if repo:
373 msg = M(self, 'repo_exists', state, group_name=group_name)
373 msg = M(self, 'repo_exists', state, group_name=group_name)
374 raise formencode.Invalid(
374 raise formencode.Invalid(
375 msg, value, state, error_dict={'group_name': msg})
375 msg, value, state, error_dict={'group_name': msg})
376 return _validator
376 return _validator
377
377
378
378
379 def ValidPassword(localizer):
379 def ValidPassword(localizer):
380 _ = localizer
380 _ = localizer
381
381
382 class _validator(formencode.validators.FancyValidator):
382 class _validator(formencode.validators.FancyValidator):
383 messages = {
383 messages = {
384 'invalid_password':
384 'invalid_password':
385 _('Invalid characters (non-ascii) in password')
385 _('Invalid characters (non-ascii) in password')
386 }
386 }
387
387
388 def _validate_python(self, value, state):
388 def _validate_python(self, value, state):
389 if value and not value.isascii():
389 if value and not value.isascii():
390 msg = M(self, 'invalid_password', state)
390 msg = M(self, 'invalid_password', state)
391 raise formencode.Invalid(msg, value, state,)
391 raise formencode.Invalid(msg, value, state,)
392 return _validator
392 return _validator
393
393
394
394
395 def ValidPasswordsMatch(
395 def ValidPasswordsMatch(
396 localizer, passwd='new_password',
396 localizer, passwd='new_password',
397 passwd_confirmation='password_confirmation'):
397 passwd_confirmation='password_confirmation'):
398 _ = localizer
398 _ = localizer
399
399
400 class _validator(formencode.validators.FancyValidator):
400 class _validator(formencode.validators.FancyValidator):
401 messages = {
401 messages = {
402 'password_mismatch': _('Passwords do not match'),
402 'password_mismatch': _('Passwords do not match'),
403 }
403 }
404
404
405 def _validate_python(self, value, state):
405 def _validate_python(self, value, state):
406
406
407 pass_val = value.get('password') or value.get(passwd)
407 pass_val = value.get('password') or value.get(passwd)
408 if pass_val != value[passwd_confirmation]:
408 if pass_val != value[passwd_confirmation]:
409 msg = M(self, 'password_mismatch', state)
409 msg = M(self, 'password_mismatch', state)
410 raise formencode.Invalid(
410 raise formencode.Invalid(
411 msg, value, state,
411 msg, value, state,
412 error_dict={passwd: msg, passwd_confirmation: msg}
412 error_dict={passwd: msg, passwd_confirmation: msg}
413 )
413 )
414 return _validator
414 return _validator
415
415
416
416
417 def ValidAuth(localizer):
417 def ValidAuth(localizer):
418 _ = localizer
418 _ = localizer
419
419
420 class _validator(formencode.validators.FancyValidator):
420 class _validator(formencode.validators.FancyValidator):
421 messages = {
421 messages = {
422 'invalid_password': _('invalid password'),
422 'invalid_password': _('invalid password'),
423 'invalid_username': _('invalid user name'),
423 'invalid_username': _('invalid user name'),
424 'disabled_account': _('Your account is disabled')
424 'disabled_account': _('Your account is disabled')
425 }
425 }
426
426
427 def _validate_python(self, value, state):
427 def _validate_python(self, value, state):
428 from rhodecode.authentication.base import authenticate, HTTP_TYPE
428 from rhodecode.authentication.base import authenticate, HTTP_TYPE
429
429
430 password = value['password']
430 password = value['password']
431 username = value['username']
431 username = value['username']
432
432
433 if not authenticate(username, password, '', HTTP_TYPE,
433 if not authenticate(username, password, '', HTTP_TYPE,
434 skip_missing=True):
434 skip_missing=True):
435 user = User.get_by_username(username)
435 user = User.get_by_username_or_primary_email(username)
436 if user and not user.active:
436 if user and not user.active:
437 log.warning('user %s is disabled', username)
437 log.warning('user %s is disabled', username)
438 msg = M(self, 'disabled_account', state)
438 msg = M(self, 'disabled_account', state)
439 raise formencode.Invalid(
439 raise formencode.Invalid(
440 msg, value, state, error_dict={'username': msg}
440 msg, value, state, error_dict={'username': msg}
441 )
441 )
442 else:
442 else:
443 log.warning('user `%s` failed to authenticate', username)
443 log.warning('user `%s` failed to authenticate', username)
444 msg = M(self, 'invalid_username', state)
444 msg = M(self, 'invalid_username', state)
445 msg2 = M(self, 'invalid_password', state)
445 msg2 = M(self, 'invalid_password', state)
446 raise formencode.Invalid(
446 raise formencode.Invalid(
447 msg, value, state,
447 msg, value, state,
448 error_dict={'username': msg, 'password': msg2}
448 error_dict={'username': msg, 'password': msg2}
449 )
449 )
450 return _validator
450 return _validator
451
451
452
452
453 def ValidRepoName(localizer, edit=False, old_data=None):
453 def ValidRepoName(localizer, edit=False, old_data=None):
454 old_data = old_data or {}
454 old_data = old_data or {}
455 _ = localizer
455 _ = localizer
456
456
457 class _validator(formencode.validators.FancyValidator):
457 class _validator(formencode.validators.FancyValidator):
458 messages = {
458 messages = {
459 'invalid_repo_name':
459 'invalid_repo_name':
460 _('Repository name %(repo)s is disallowed'),
460 _('Repository name %(repo)s is disallowed'),
461 # top level
461 # top level
462 'repository_exists': _('Repository with name %(repo)s '
462 'repository_exists': _('Repository with name %(repo)s '
463 'already exists'),
463 'already exists'),
464 'group_exists': _('Repository group with name "%(repo)s" '
464 'group_exists': _('Repository group with name "%(repo)s" '
465 'already exists'),
465 'already exists'),
466 # inside a group
466 # inside a group
467 'repository_in_group_exists': _('Repository with name %(repo)s '
467 'repository_in_group_exists': _('Repository with name %(repo)s '
468 'exists in group "%(group)s"'),
468 'exists in group "%(group)s"'),
469 'group_in_group_exists': _(
469 'group_in_group_exists': _(
470 'Repository group with name "%(repo)s" '
470 'Repository group with name "%(repo)s" '
471 'exists in group "%(group)s"'),
471 'exists in group "%(group)s"'),
472 }
472 }
473
473
474 def _convert_to_python(self, value, state):
474 def _convert_to_python(self, value, state):
475 repo_name = repo_name_slug(value.get('repo_name', ''))
475 repo_name = repo_name_slug(value.get('repo_name', ''))
476 repo_group = value.get('repo_group')
476 repo_group = value.get('repo_group')
477 if repo_group:
477 if repo_group:
478 gr = RepoGroup.get(repo_group)
478 gr = RepoGroup.get(repo_group)
479 group_path = gr.full_path
479 group_path = gr.full_path
480 group_name = gr.group_name
480 group_name = gr.group_name
481 # value needs to be aware of group name in order to check
481 # value needs to be aware of group name in order to check
482 # db key This is an actual just the name to store in the
482 # db key This is an actual just the name to store in the
483 # database
483 # database
484 repo_name_full = group_path + RepoGroup.url_sep() + repo_name
484 repo_name_full = group_path + RepoGroup.url_sep() + repo_name
485 else:
485 else:
486 group_name = group_path = ''
486 group_name = group_path = ''
487 repo_name_full = repo_name
487 repo_name_full = repo_name
488
488
489 value['repo_name'] = repo_name
489 value['repo_name'] = repo_name
490 value['repo_name_full'] = repo_name_full
490 value['repo_name_full'] = repo_name_full
491 value['group_path'] = group_path
491 value['group_path'] = group_path
492 value['group_name'] = group_name
492 value['group_name'] = group_name
493 return value
493 return value
494
494
495 def _validate_python(self, value, state):
495 def _validate_python(self, value, state):
496
496
497 repo_name = value.get('repo_name')
497 repo_name = value.get('repo_name')
498 repo_name_full = value.get('repo_name_full')
498 repo_name_full = value.get('repo_name_full')
499 group_path = value.get('group_path')
499 group_path = value.get('group_path')
500 group_name = value.get('group_name')
500 group_name = value.get('group_name')
501
501
502 if repo_name in [ADMIN_PREFIX, '']:
502 if repo_name in [ADMIN_PREFIX, '']:
503 msg = M(self, 'invalid_repo_name', state, repo=repo_name)
503 msg = M(self, 'invalid_repo_name', state, repo=repo_name)
504 raise formencode.Invalid(
504 raise formencode.Invalid(
505 msg, value, state, error_dict={'repo_name': msg})
505 msg, value, state, error_dict={'repo_name': msg})
506
506
507 rename = old_data.get('repo_name') != repo_name_full
507 rename = old_data.get('repo_name') != repo_name_full
508 create = not edit
508 create = not edit
509 if rename or create:
509 if rename or create:
510
510
511 if group_path:
511 if group_path:
512 if Repository.get_by_repo_name(repo_name_full):
512 if Repository.get_by_repo_name(repo_name_full):
513 msg = M(self, 'repository_in_group_exists', state,
513 msg = M(self, 'repository_in_group_exists', state,
514 repo=repo_name, group=group_name)
514 repo=repo_name, group=group_name)
515 raise formencode.Invalid(
515 raise formencode.Invalid(
516 msg, value, state, error_dict={'repo_name': msg})
516 msg, value, state, error_dict={'repo_name': msg})
517 if RepoGroup.get_by_group_name(repo_name_full):
517 if RepoGroup.get_by_group_name(repo_name_full):
518 msg = M(self, 'group_in_group_exists', state,
518 msg = M(self, 'group_in_group_exists', state,
519 repo=repo_name, group=group_name)
519 repo=repo_name, group=group_name)
520 raise formencode.Invalid(
520 raise formencode.Invalid(
521 msg, value, state, error_dict={'repo_name': msg})
521 msg, value, state, error_dict={'repo_name': msg})
522 else:
522 else:
523 if RepoGroup.get_by_group_name(repo_name_full):
523 if RepoGroup.get_by_group_name(repo_name_full):
524 msg = M(self, 'group_exists', state, repo=repo_name)
524 msg = M(self, 'group_exists', state, repo=repo_name)
525 raise formencode.Invalid(
525 raise formencode.Invalid(
526 msg, value, state, error_dict={'repo_name': msg})
526 msg, value, state, error_dict={'repo_name': msg})
527
527
528 if Repository.get_by_repo_name(repo_name_full):
528 if Repository.get_by_repo_name(repo_name_full):
529 msg = M(
529 msg = M(
530 self, 'repository_exists', state, repo=repo_name)
530 self, 'repository_exists', state, repo=repo_name)
531 raise formencode.Invalid(
531 raise formencode.Invalid(
532 msg, value, state, error_dict={'repo_name': msg})
532 msg, value, state, error_dict={'repo_name': msg})
533 return value
533 return value
534 return _validator
534 return _validator
535
535
536
536
537 def ValidForkName(localizer, *args, **kwargs):
537 def ValidForkName(localizer, *args, **kwargs):
538 _ = localizer
538 _ = localizer
539
539
540 return ValidRepoName(localizer, *args, **kwargs)
540 return ValidRepoName(localizer, *args, **kwargs)
541
541
542
542
543 def SlugifyName(localizer):
543 def SlugifyName(localizer):
544 _ = localizer
544 _ = localizer
545
545
546 class _validator(formencode.validators.FancyValidator):
546 class _validator(formencode.validators.FancyValidator):
547
547
548 def _convert_to_python(self, value, state):
548 def _convert_to_python(self, value, state):
549 return repo_name_slug(value)
549 return repo_name_slug(value)
550
550
551 def _validate_python(self, value, state):
551 def _validate_python(self, value, state):
552 pass
552 pass
553 return _validator
553 return _validator
554
554
555
555
556 def CannotHaveGitSuffix(localizer):
556 def CannotHaveGitSuffix(localizer):
557 _ = localizer
557 _ = localizer
558
558
559 class _validator(formencode.validators.FancyValidator):
559 class _validator(formencode.validators.FancyValidator):
560 messages = {
560 messages = {
561 'has_git_suffix':
561 'has_git_suffix':
562 _('Repository name cannot end with .git'),
562 _('Repository name cannot end with .git'),
563 }
563 }
564
564
565 def _convert_to_python(self, value, state):
565 def _convert_to_python(self, value, state):
566 return value
566 return value
567
567
568 def _validate_python(self, value, state):
568 def _validate_python(self, value, state):
569 if value and value.endswith('.git'):
569 if value and value.endswith('.git'):
570 msg = M(
570 msg = M(
571 self, 'has_git_suffix', state)
571 self, 'has_git_suffix', state)
572 raise formencode.Invalid(
572 raise formencode.Invalid(
573 msg, value, state, error_dict={'repo_name': msg})
573 msg, value, state, error_dict={'repo_name': msg})
574 return _validator
574 return _validator
575
575
576
576
577 def ValidCloneUri(localizer):
577 def ValidCloneUri(localizer):
578 _ = localizer
578 _ = localizer
579
579
580 class InvalidCloneUrl(Exception):
580 class InvalidCloneUrl(Exception):
581 allowed_prefixes = ()
581 allowed_prefixes = ()
582
582
583 def url_handler(repo_type, url):
583 def url_handler(repo_type, url):
584 config = make_db_config(clear_session=False)
584 config = make_db_config(clear_session=False)
585 if repo_type == 'hg':
585 if repo_type == 'hg':
586 allowed_prefixes = ('http', 'svn+http', 'git+http')
586 allowed_prefixes = ('http', 'svn+http', 'git+http')
587
587
588 if 'http' in url[:4]:
588 if 'http' in url[:4]:
589 # initially check if it's at least the proper URL
589 # initially check if it's at least the proper URL
590 # or does it pass basic auth
590 # or does it pass basic auth
591 MercurialRepository.check_url(url, config)
591 MercurialRepository.check_url(url, config)
592 elif 'svn+http' in url[:8]: # svn->hg import
592 elif 'svn+http' in url[:8]: # svn->hg import
593 SubversionRepository.check_url(url, config)
593 SubversionRepository.check_url(url, config)
594 elif 'git+http' in url[:8]: # git->hg import
594 elif 'git+http' in url[:8]: # git->hg import
595 raise NotImplementedError()
595 raise NotImplementedError()
596 else:
596 else:
597 exc = InvalidCloneUrl('Clone from URI %s not allowed. '
597 exc = InvalidCloneUrl('Clone from URI %s not allowed. '
598 'Allowed url must start with one of %s'
598 'Allowed url must start with one of %s'
599 % (url, ','.join(allowed_prefixes)))
599 % (url, ','.join(allowed_prefixes)))
600 exc.allowed_prefixes = allowed_prefixes
600 exc.allowed_prefixes = allowed_prefixes
601 raise exc
601 raise exc
602
602
603 elif repo_type == 'git':
603 elif repo_type == 'git':
604 allowed_prefixes = ('http', 'svn+http', 'hg+http')
604 allowed_prefixes = ('http', 'svn+http', 'hg+http')
605 if 'http' in url[:4]:
605 if 'http' in url[:4]:
606 # initially check if it's at least the proper URL
606 # initially check if it's at least the proper URL
607 # or does it pass basic auth
607 # or does it pass basic auth
608 GitRepository.check_url(url, config)
608 GitRepository.check_url(url, config)
609 elif 'svn+http' in url[:8]: # svn->git import
609 elif 'svn+http' in url[:8]: # svn->git import
610 raise NotImplementedError()
610 raise NotImplementedError()
611 elif 'hg+http' in url[:8]: # hg->git import
611 elif 'hg+http' in url[:8]: # hg->git import
612 raise NotImplementedError()
612 raise NotImplementedError()
613 else:
613 else:
614 exc = InvalidCloneUrl('Clone from URI %s not allowed. '
614 exc = InvalidCloneUrl('Clone from URI %s not allowed. '
615 'Allowed url must start with one of %s'
615 'Allowed url must start with one of %s'
616 % (url, ','.join(allowed_prefixes)))
616 % (url, ','.join(allowed_prefixes)))
617 exc.allowed_prefixes = allowed_prefixes
617 exc.allowed_prefixes = allowed_prefixes
618 raise exc
618 raise exc
619
619
620 class _validator(formencode.validators.FancyValidator):
620 class _validator(formencode.validators.FancyValidator):
621 messages = {
621 messages = {
622 'clone_uri': _('invalid clone url or credentials for %(rtype)s repository'),
622 'clone_uri': _('invalid clone url or credentials for %(rtype)s repository'),
623 'invalid_clone_uri': _(
623 'invalid_clone_uri': _(
624 'Invalid clone url, provide a valid clone '
624 'Invalid clone url, provide a valid clone '
625 'url starting with one of %(allowed_prefixes)s')
625 'url starting with one of %(allowed_prefixes)s')
626 }
626 }
627
627
628 def _validate_python(self, value, state):
628 def _validate_python(self, value, state):
629 repo_type = value.get('repo_type')
629 repo_type = value.get('repo_type')
630 url = value.get('clone_uri')
630 url = value.get('clone_uri')
631
631
632 if url:
632 if url:
633 try:
633 try:
634 url_handler(repo_type, url)
634 url_handler(repo_type, url)
635 except InvalidCloneUrl as e:
635 except InvalidCloneUrl as e:
636 log.warning(e)
636 log.warning(e)
637 msg = M(self, 'invalid_clone_uri', state, rtype=repo_type,
637 msg = M(self, 'invalid_clone_uri', state, rtype=repo_type,
638 allowed_prefixes=','.join(e.allowed_prefixes))
638 allowed_prefixes=','.join(e.allowed_prefixes))
639 raise formencode.Invalid(msg, value, state,
639 raise formencode.Invalid(msg, value, state,
640 error_dict={'clone_uri': msg})
640 error_dict={'clone_uri': msg})
641 except Exception:
641 except Exception:
642 log.exception('Url validation failed')
642 log.exception('Url validation failed')
643 msg = M(self, 'clone_uri', state, rtype=repo_type)
643 msg = M(self, 'clone_uri', state, rtype=repo_type)
644 raise formencode.Invalid(msg, value, state,
644 raise formencode.Invalid(msg, value, state,
645 error_dict={'clone_uri': msg})
645 error_dict={'clone_uri': msg})
646 return _validator
646 return _validator
647
647
648
648
649 def ValidForkType(localizer, old_data=None):
649 def ValidForkType(localizer, old_data=None):
650 _ = localizer
650 _ = localizer
651 old_data = old_data or {}
651 old_data = old_data or {}
652
652
653 class _validator(formencode.validators.FancyValidator):
653 class _validator(formencode.validators.FancyValidator):
654 messages = {
654 messages = {
655 'invalid_fork_type': _('Fork have to be the same type as parent')
655 'invalid_fork_type': _('Fork have to be the same type as parent')
656 }
656 }
657
657
658 def _validate_python(self, value, state):
658 def _validate_python(self, value, state):
659 if old_data['repo_type'] != value:
659 if old_data['repo_type'] != value:
660 msg = M(self, 'invalid_fork_type', state)
660 msg = M(self, 'invalid_fork_type', state)
661 raise formencode.Invalid(
661 raise formencode.Invalid(
662 msg, value, state, error_dict={'repo_type': msg}
662 msg, value, state, error_dict={'repo_type': msg}
663 )
663 )
664 return _validator
664 return _validator
665
665
666
666
667 def CanWriteGroup(localizer, old_data=None):
667 def CanWriteGroup(localizer, old_data=None):
668 _ = localizer
668 _ = localizer
669
669
670 class _validator(formencode.validators.FancyValidator):
670 class _validator(formencode.validators.FancyValidator):
671 messages = {
671 messages = {
672 'permission_denied': _(
672 'permission_denied': _(
673 "You do not have the permission "
673 "You do not have the permission "
674 "to create repositories in this group."),
674 "to create repositories in this group."),
675 'permission_denied_root': _(
675 'permission_denied_root': _(
676 "You do not have the permission to store repositories in "
676 "You do not have the permission to store repositories in "
677 "the root location.")
677 "the root location.")
678 }
678 }
679
679
680 def _convert_to_python(self, value, state):
680 def _convert_to_python(self, value, state):
681 # root location
681 # root location
682 if value in [-1, "-1"]:
682 if value in [-1, "-1"]:
683 return None
683 return None
684 return value
684 return value
685
685
686 def _validate_python(self, value, state):
686 def _validate_python(self, value, state):
687 gr = RepoGroup.get(value)
687 gr = RepoGroup.get(value)
688 gr_name = gr.group_name if gr else None # None means ROOT location
688 gr_name = gr.group_name if gr else None # None means ROOT location
689 # create repositories with write permission on group is set to true
689 # create repositories with write permission on group is set to true
690 create_on_write = HasPermissionAny(
690 create_on_write = HasPermissionAny(
691 'hg.create.write_on_repogroup.true')()
691 'hg.create.write_on_repogroup.true')()
692 group_admin = HasRepoGroupPermissionAny('group.admin')(
692 group_admin = HasRepoGroupPermissionAny('group.admin')(
693 gr_name, 'can write into group validator')
693 gr_name, 'can write into group validator')
694 group_write = HasRepoGroupPermissionAny('group.write')(
694 group_write = HasRepoGroupPermissionAny('group.write')(
695 gr_name, 'can write into group validator')
695 gr_name, 'can write into group validator')
696 forbidden = not (group_admin or (group_write and create_on_write))
696 forbidden = not (group_admin or (group_write and create_on_write))
697 can_create_repos = HasPermissionAny(
697 can_create_repos = HasPermissionAny(
698 'hg.admin', 'hg.create.repository')
698 'hg.admin', 'hg.create.repository')
699 gid = (old_data['repo_group'].get('group_id')
699 gid = (old_data['repo_group'].get('group_id')
700 if (old_data and 'repo_group' in old_data) else None)
700 if (old_data and 'repo_group' in old_data) else None)
701 value_changed = gid != safe_int(value)
701 value_changed = gid != safe_int(value)
702 new = not old_data
702 new = not old_data
703 # do check if we changed the value, there's a case that someone got
703 # do check if we changed the value, there's a case that someone got
704 # revoked write permissions to a repository, he still created, we
704 # revoked write permissions to a repository, he still created, we
705 # don't need to check permission if he didn't change the value of
705 # don't need to check permission if he didn't change the value of
706 # groups in form box
706 # groups in form box
707 if value_changed or new:
707 if value_changed or new:
708 # parent group need to be existing
708 # parent group need to be existing
709 if gr and forbidden:
709 if gr and forbidden:
710 msg = M(self, 'permission_denied', state)
710 msg = M(self, 'permission_denied', state)
711 raise formencode.Invalid(
711 raise formencode.Invalid(
712 msg, value, state, error_dict={'repo_type': msg}
712 msg, value, state, error_dict={'repo_type': msg}
713 )
713 )
714 # check if we can write to root location !
714 # check if we can write to root location !
715 elif gr is None and not can_create_repos():
715 elif gr is None and not can_create_repos():
716 msg = M(self, 'permission_denied_root', state)
716 msg = M(self, 'permission_denied_root', state)
717 raise formencode.Invalid(
717 raise formencode.Invalid(
718 msg, value, state, error_dict={'repo_type': msg}
718 msg, value, state, error_dict={'repo_type': msg}
719 )
719 )
720 return _validator
720 return _validator
721
721
722
722
723 def ValidPerms(localizer, type_='repo'):
723 def ValidPerms(localizer, type_='repo'):
724 _ = localizer
724 _ = localizer
725 if type_ == 'repo_group':
725 if type_ == 'repo_group':
726 EMPTY_PERM = 'group.none'
726 EMPTY_PERM = 'group.none'
727 elif type_ == 'repo':
727 elif type_ == 'repo':
728 EMPTY_PERM = 'repository.none'
728 EMPTY_PERM = 'repository.none'
729 elif type_ == 'user_group':
729 elif type_ == 'user_group':
730 EMPTY_PERM = 'usergroup.none'
730 EMPTY_PERM = 'usergroup.none'
731
731
732 class _validator(formencode.validators.FancyValidator):
732 class _validator(formencode.validators.FancyValidator):
733 messages = {
733 messages = {
734 'perm_new_member_name':
734 'perm_new_member_name':
735 _('This username or user group name is not valid')
735 _('This username or user group name is not valid')
736 }
736 }
737
737
738 def _convert_to_python(self, value, state):
738 def _convert_to_python(self, value, state):
739 perm_updates = OrderedSet()
739 perm_updates = OrderedSet()
740 perm_additions = OrderedSet()
740 perm_additions = OrderedSet()
741 perm_deletions = OrderedSet()
741 perm_deletions = OrderedSet()
742 # build a list of permission to update/delete and new permission
742 # build a list of permission to update/delete and new permission
743
743
744 # Read the perm_new_member/perm_del_member attributes and group
744 # Read the perm_new_member/perm_del_member attributes and group
745 # them by they IDs
745 # them by they IDs
746 new_perms_group = collections.defaultdict(dict)
746 new_perms_group = collections.defaultdict(dict)
747 del_perms_group = collections.defaultdict(dict)
747 del_perms_group = collections.defaultdict(dict)
748 for k, v in list(value.copy().items()):
748 for k, v in list(value.copy().items()):
749 if k.startswith('perm_del_member'):
749 if k.startswith('perm_del_member'):
750 # delete from org storage so we don't process that later
750 # delete from org storage so we don't process that later
751 del value[k]
751 del value[k]
752 # part is `id`, `type`
752 # part is `id`, `type`
753 _type, part = k.split('perm_del_member_')
753 _type, part = k.split('perm_del_member_')
754 args = part.split('_')
754 args = part.split('_')
755 if len(args) == 2:
755 if len(args) == 2:
756 _key, pos = args
756 _key, pos = args
757 del_perms_group[pos][_key] = v
757 del_perms_group[pos][_key] = v
758 if k.startswith('perm_new_member'):
758 if k.startswith('perm_new_member'):
759 # delete from org storage so we don't process that later
759 # delete from org storage so we don't process that later
760 del value[k]
760 del value[k]
761 # part is `id`, `type`, `perm`
761 # part is `id`, `type`, `perm`
762 _type, part = k.split('perm_new_member_')
762 _type, part = k.split('perm_new_member_')
763 args = part.split('_')
763 args = part.split('_')
764 if len(args) == 2:
764 if len(args) == 2:
765 _key, pos = args
765 _key, pos = args
766 new_perms_group[pos][_key] = v
766 new_perms_group[pos][_key] = v
767
767
768 # store the deletes
768 # store the deletes
769 for k in sorted(del_perms_group.keys()):
769 for k in sorted(del_perms_group.keys()):
770 perm_dict = del_perms_group[k]
770 perm_dict = del_perms_group[k]
771 del_member = perm_dict.get('id')
771 del_member = perm_dict.get('id')
772 del_type = perm_dict.get('type')
772 del_type = perm_dict.get('type')
773 if del_member and del_type:
773 if del_member and del_type:
774 perm_deletions.add(
774 perm_deletions.add(
775 (del_member, None, del_type))
775 (del_member, None, del_type))
776
776
777 # store additions in order of how they were added in web form
777 # store additions in order of how they were added in web form
778 for k in sorted(new_perms_group.keys()):
778 for k in sorted(new_perms_group.keys()):
779 perm_dict = new_perms_group[k]
779 perm_dict = new_perms_group[k]
780 new_member = perm_dict.get('id')
780 new_member = perm_dict.get('id')
781 new_type = perm_dict.get('type')
781 new_type = perm_dict.get('type')
782 new_perm = perm_dict.get('perm')
782 new_perm = perm_dict.get('perm')
783 if new_member and new_perm and new_type:
783 if new_member and new_perm and new_type:
784 perm_additions.add(
784 perm_additions.add(
785 (new_member, new_perm, new_type))
785 (new_member, new_perm, new_type))
786
786
787 # get updates of permissions
787 # get updates of permissions
788 # (read the existing radio button states)
788 # (read the existing radio button states)
789 default_user_id = User.get_default_user_id()
789 default_user_id = User.get_default_user_id()
790
790
791 for k, update_value in list(value.items()):
791 for k, update_value in list(value.items()):
792 if k.startswith('u_perm_') or k.startswith('g_perm_'):
792 if k.startswith('u_perm_') or k.startswith('g_perm_'):
793 obj_type = k[0]
793 obj_type = k[0]
794 obj_id = k[7:]
794 obj_id = k[7:]
795 update_type = {'u': 'user',
795 update_type = {'u': 'user',
796 'g': 'user_group'}[obj_type]
796 'g': 'user_group'}[obj_type]
797
797
798 if obj_type == 'u' and safe_int(obj_id) == default_user_id:
798 if obj_type == 'u' and safe_int(obj_id) == default_user_id:
799 if str2bool(value.get('repo_private')):
799 if str2bool(value.get('repo_private')):
800 # prevent from updating default user permissions
800 # prevent from updating default user permissions
801 # when this repository is marked as private
801 # when this repository is marked as private
802 update_value = EMPTY_PERM
802 update_value = EMPTY_PERM
803
803
804 perm_updates.add(
804 perm_updates.add(
805 (obj_id, update_value, update_type))
805 (obj_id, update_value, update_type))
806
806
807 value['perm_additions'] = [] # propagated later
807 value['perm_additions'] = [] # propagated later
808 value['perm_updates'] = list(perm_updates)
808 value['perm_updates'] = list(perm_updates)
809 value['perm_deletions'] = list(perm_deletions)
809 value['perm_deletions'] = list(perm_deletions)
810
810
811 updates_map = dict(
811 updates_map = dict(
812 (x[0], (x[1], x[2])) for x in value['perm_updates'])
812 (x[0], (x[1], x[2])) for x in value['perm_updates'])
813 # make sure Additions don't override updates.
813 # make sure Additions don't override updates.
814 for member_id, perm, member_type in list(perm_additions):
814 for member_id, perm, member_type in list(perm_additions):
815 if member_id in updates_map:
815 if member_id in updates_map:
816 perm = updates_map[member_id][0]
816 perm = updates_map[member_id][0]
817 value['perm_additions'].append((member_id, perm, member_type))
817 value['perm_additions'].append((member_id, perm, member_type))
818
818
819 # on new entries validate users they exist and they are active !
819 # on new entries validate users they exist and they are active !
820 # this leaves feedback to the form
820 # this leaves feedback to the form
821 try:
821 try:
822 if member_type == 'user':
822 if member_type == 'user':
823 User.query()\
823 User.query()\
824 .filter(User.active == true())\
824 .filter(User.active == true())\
825 .filter(User.user_id == member_id).one()
825 .filter(User.user_id == member_id).one()
826 if member_type == 'user_group':
826 if member_type == 'user_group':
827 UserGroup.query()\
827 UserGroup.query()\
828 .filter(UserGroup.users_group_active == true())\
828 .filter(UserGroup.users_group_active == true())\
829 .filter(UserGroup.users_group_id == member_id)\
829 .filter(UserGroup.users_group_id == member_id)\
830 .one()
830 .one()
831
831
832 except Exception:
832 except Exception:
833 log.exception('Updated permission failed: org_exc:')
833 log.exception('Updated permission failed: org_exc:')
834 msg = M(self, 'perm_new_member_type', state)
834 msg = M(self, 'perm_new_member_type', state)
835 raise formencode.Invalid(
835 raise formencode.Invalid(
836 msg, value, state, error_dict={
836 msg, value, state, error_dict={
837 'perm_new_member_name': msg}
837 'perm_new_member_name': msg}
838 )
838 )
839 return value
839 return value
840 return _validator
840 return _validator
841
841
842
842
843 def ValidPath(localizer):
843 def ValidPath(localizer):
844 _ = localizer
844 _ = localizer
845
845
846 class _validator(formencode.validators.FancyValidator):
846 class _validator(formencode.validators.FancyValidator):
847 messages = {
847 messages = {
848 'invalid_path': _('This is not a valid path')
848 'invalid_path': _('This is not a valid path')
849 }
849 }
850
850
851 def _validate_python(self, value, state):
851 def _validate_python(self, value, state):
852 if not os.path.isdir(value):
852 if not os.path.isdir(value):
853 msg = M(self, 'invalid_path', state)
853 msg = M(self, 'invalid_path', state)
854 raise formencode.Invalid(
854 raise formencode.Invalid(
855 msg, value, state, error_dict={'paths_root_path': msg}
855 msg, value, state, error_dict={'paths_root_path': msg}
856 )
856 )
857 return _validator
857 return _validator
858
858
859
859
860 def UniqSystemEmail(localizer, old_data=None):
860 def UniqSystemEmail(localizer, old_data=None):
861 _ = localizer
861 _ = localizer
862 old_data = old_data or {}
862 old_data = old_data or {}
863
863
864 class _validator(formencode.validators.FancyValidator):
864 class _validator(formencode.validators.FancyValidator):
865 messages = {
865 messages = {
866 'email_taken': _('This e-mail address is already taken')
866 'email_taken': _('This e-mail address is already taken')
867 }
867 }
868
868
869 def _convert_to_python(self, value, state):
869 def _convert_to_python(self, value, state):
870 return value.lower()
870 return value.lower()
871
871
872 def _validate_python(self, value, state):
872 def _validate_python(self, value, state):
873 if (old_data.get('email') or '').lower() != value:
873 if (old_data.get('email') or '').lower() != value:
874 user = User.get_by_email(value, case_insensitive=True)
874 user = User.get_by_email(value, case_insensitive=True)
875 if user:
875 if user:
876 msg = M(self, 'email_taken', state)
876 msg = M(self, 'email_taken', state)
877 raise formencode.Invalid(
877 raise formencode.Invalid(
878 msg, value, state, error_dict={'email': msg}
878 msg, value, state, error_dict={'email': msg}
879 )
879 )
880 return _validator
880 return _validator
881
881
882
882
883 def ValidSystemEmail(localizer):
883 def ValidSystemEmail(localizer):
884 _ = localizer
884 _ = localizer
885
885
886 class _validator(formencode.validators.FancyValidator):
886 class _validator(formencode.validators.FancyValidator):
887 messages = {
887 messages = {
888 'non_existing_email': _('e-mail "%(email)s" does not exist.')
888 'non_existing_email': _('e-mail "%(email)s" does not exist.')
889 }
889 }
890
890
891 def _convert_to_python(self, value, state):
891 def _convert_to_python(self, value, state):
892 return value.lower()
892 return value.lower()
893
893
894 def _validate_python(self, value, state):
894 def _validate_python(self, value, state):
895 user = User.get_by_email(value, case_insensitive=True)
895 user = User.get_by_email(value, case_insensitive=True)
896 if user is None:
896 if user is None:
897 msg = M(self, 'non_existing_email', state, email=value)
897 msg = M(self, 'non_existing_email', state, email=value)
898 raise formencode.Invalid(
898 raise formencode.Invalid(
899 msg, value, state, error_dict={'email': msg}
899 msg, value, state, error_dict={'email': msg}
900 )
900 )
901 return _validator
901 return _validator
902
902
903
903
904 def NotReviewedRevisions(localizer, repo_id):
904 def NotReviewedRevisions(localizer, repo_id):
905 _ = localizer
905 _ = localizer
906 class _validator(formencode.validators.FancyValidator):
906 class _validator(formencode.validators.FancyValidator):
907 messages = {
907 messages = {
908 'rev_already_reviewed':
908 'rev_already_reviewed':
909 _('Revisions %(revs)s are already part of pull request '
909 _('Revisions %(revs)s are already part of pull request '
910 'or have set status'),
910 'or have set status'),
911 }
911 }
912
912
913 def _validate_python(self, value, state):
913 def _validate_python(self, value, state):
914 # check revisions if they are not reviewed, or a part of another
914 # check revisions if they are not reviewed, or a part of another
915 # pull request
915 # pull request
916 statuses = ChangesetStatus.query()\
916 statuses = ChangesetStatus.query()\
917 .filter(ChangesetStatus.revision.in_(value))\
917 .filter(ChangesetStatus.revision.in_(value))\
918 .filter(ChangesetStatus.repo_id == repo_id)\
918 .filter(ChangesetStatus.repo_id == repo_id)\
919 .all()
919 .all()
920
920
921 errors = []
921 errors = []
922 for status in statuses:
922 for status in statuses:
923 if status.pull_request_id:
923 if status.pull_request_id:
924 errors.append(['pull_req', status.revision[:12]])
924 errors.append(['pull_req', status.revision[:12]])
925 elif status.status:
925 elif status.status:
926 errors.append(['status', status.revision[:12]])
926 errors.append(['status', status.revision[:12]])
927
927
928 if errors:
928 if errors:
929 revs = ','.join([x[1] for x in errors])
929 revs = ','.join([x[1] for x in errors])
930 msg = M(self, 'rev_already_reviewed', state, revs=revs)
930 msg = M(self, 'rev_already_reviewed', state, revs=revs)
931 raise formencode.Invalid(
931 raise formencode.Invalid(
932 msg, value, state, error_dict={'revisions': revs})
932 msg, value, state, error_dict={'revisions': revs})
933
933
934 return _validator
934 return _validator
935
935
936
936
937 def ValidIp(localizer):
937 def ValidIp(localizer):
938 _ = localizer
938 _ = localizer
939
939
940 class _validator(CIDR):
940 class _validator(CIDR):
941 messages = {
941 messages = {
942 'badFormat': _('Please enter a valid IPv4 or IpV6 address'),
942 'badFormat': _('Please enter a valid IPv4 or IpV6 address'),
943 'illegalBits': _(
943 'illegalBits': _(
944 'The network size (bits) must be within the range '
944 'The network size (bits) must be within the range '
945 'of 0-32 (not %(bits)r)'),
945 'of 0-32 (not %(bits)r)'),
946 }
946 }
947
947
948 # we override the default to_python() call
948 # we override the default to_python() call
949 def to_python(self, value, state):
949 def to_python(self, value, state):
950 v = super().to_python(value, state)
950 v = super().to_python(value, state)
951 v = safe_str(v.strip())
951 v = safe_str(v.strip())
952 net = ipaddress.ip_network(address=v, strict=False)
952 net = ipaddress.ip_network(address=v, strict=False)
953 return str(net)
953 return str(net)
954
954
955 def _validate_python(self, value, state):
955 def _validate_python(self, value, state):
956 try:
956 try:
957 addr = safe_str(value.strip())
957 addr = safe_str(value.strip())
958 # this raises an ValueError if address is not IpV4 or IpV6
958 # this raises an ValueError if address is not IpV4 or IpV6
959 ipaddress.ip_network(addr, strict=False)
959 ipaddress.ip_network(addr, strict=False)
960 except ValueError:
960 except ValueError:
961 raise formencode.Invalid(self.message('badFormat', state),
961 raise formencode.Invalid(self.message('badFormat', state),
962 value, state)
962 value, state)
963 return _validator
963 return _validator
964
964
965
965
966 def FieldKey(localizer):
966 def FieldKey(localizer):
967 _ = localizer
967 _ = localizer
968
968
969 class _validator(formencode.validators.FancyValidator):
969 class _validator(formencode.validators.FancyValidator):
970 messages = {
970 messages = {
971 'badFormat': _(
971 'badFormat': _(
972 'Key name can only consist of letters, '
972 'Key name can only consist of letters, '
973 'underscore, dash or numbers'),
973 'underscore, dash or numbers'),
974 }
974 }
975
975
976 def _validate_python(self, value, state):
976 def _validate_python(self, value, state):
977 if not re.match('[a-zA-Z0-9_-]+$', value):
977 if not re.match('[a-zA-Z0-9_-]+$', value):
978 raise formencode.Invalid(self.message('badFormat', state),
978 raise formencode.Invalid(self.message('badFormat', state),
979 value, state)
979 value, state)
980 return _validator
980 return _validator
981
981
982
982
983 def ValidAuthPlugins(localizer):
983 def ValidAuthPlugins(localizer):
984 _ = localizer
984 _ = localizer
985
985
986 class _validator(formencode.validators.FancyValidator):
986 class _validator(formencode.validators.FancyValidator):
987 messages = {
987 messages = {
988 'import_duplicate': _(
988 'import_duplicate': _(
989 'Plugins %(loaded)s and %(next_to_load)s '
989 'Plugins %(loaded)s and %(next_to_load)s '
990 'both export the same name'),
990 'both export the same name'),
991 'missing_includeme': _(
991 'missing_includeme': _(
992 'The plugin "%(plugin_id)s" is missing an includeme '
992 'The plugin "%(plugin_id)s" is missing an includeme '
993 'function.'),
993 'function.'),
994 'import_error': _(
994 'import_error': _(
995 'Can not load plugin "%(plugin_id)s"'),
995 'Can not load plugin "%(plugin_id)s"'),
996 'no_plugin': _(
996 'no_plugin': _(
997 'No plugin available with ID "%(plugin_id)s"'),
997 'No plugin available with ID "%(plugin_id)s"'),
998 }
998 }
999
999
1000 def _convert_to_python(self, value, state):
1000 def _convert_to_python(self, value, state):
1001 # filter empty values
1001 # filter empty values
1002 return [s for s in value if s not in [None, '']]
1002 return [s for s in value if s not in [None, '']]
1003
1003
1004 def _validate_legacy_plugin_id(self, plugin_id, value, state):
1004 def _validate_legacy_plugin_id(self, plugin_id, value, state):
1005 """
1005 """
1006 Validates that the plugin import works. It also checks that the
1006 Validates that the plugin import works. It also checks that the
1007 plugin has an includeme attribute.
1007 plugin has an includeme attribute.
1008 """
1008 """
1009 try:
1009 try:
1010 plugin = _import_legacy_plugin(plugin_id)
1010 plugin = _import_legacy_plugin(plugin_id)
1011 except Exception as e:
1011 except Exception as e:
1012 log.exception(
1012 log.exception(
1013 'Exception during import of auth legacy plugin "{}"'
1013 'Exception during import of auth legacy plugin "{}"'
1014 .format(plugin_id))
1014 .format(plugin_id))
1015 msg = M(self, 'import_error', state, plugin_id=plugin_id)
1015 msg = M(self, 'import_error', state, plugin_id=plugin_id)
1016 raise formencode.Invalid(msg, value, state)
1016 raise formencode.Invalid(msg, value, state)
1017
1017
1018 if not hasattr(plugin, 'includeme'):
1018 if not hasattr(plugin, 'includeme'):
1019 msg = M(self, 'missing_includeme', state, plugin_id=plugin_id)
1019 msg = M(self, 'missing_includeme', state, plugin_id=plugin_id)
1020 raise formencode.Invalid(msg, value, state)
1020 raise formencode.Invalid(msg, value, state)
1021
1021
1022 return plugin
1022 return plugin
1023
1023
1024 def _validate_plugin_id(self, plugin_id, value, state):
1024 def _validate_plugin_id(self, plugin_id, value, state):
1025 """
1025 """
1026 Plugins are already imported during app start up. Therefore this
1026 Plugins are already imported during app start up. Therefore this
1027 validation only retrieves the plugin from the plugin registry and
1027 validation only retrieves the plugin from the plugin registry and
1028 if it returns something not None everything is OK.
1028 if it returns something not None everything is OK.
1029 """
1029 """
1030 plugin = loadplugin(plugin_id)
1030 plugin = loadplugin(plugin_id)
1031
1031
1032 if plugin is None:
1032 if plugin is None:
1033 msg = M(self, 'no_plugin', state, plugin_id=plugin_id)
1033 msg = M(self, 'no_plugin', state, plugin_id=plugin_id)
1034 raise formencode.Invalid(msg, value, state)
1034 raise formencode.Invalid(msg, value, state)
1035
1035
1036 return plugin
1036 return plugin
1037
1037
1038 def _validate_python(self, value, state):
1038 def _validate_python(self, value, state):
1039 unique_names = {}
1039 unique_names = {}
1040 for plugin_id in value:
1040 for plugin_id in value:
1041
1041
1042 # Validate legacy or normal plugin.
1042 # Validate legacy or normal plugin.
1043 if plugin_id.startswith(legacy_plugin_prefix):
1043 if plugin_id.startswith(legacy_plugin_prefix):
1044 plugin = self._validate_legacy_plugin_id(
1044 plugin = self._validate_legacy_plugin_id(
1045 plugin_id, value, state)
1045 plugin_id, value, state)
1046 else:
1046 else:
1047 plugin = self._validate_plugin_id(plugin_id, value, state)
1047 plugin = self._validate_plugin_id(plugin_id, value, state)
1048
1048
1049 # Only allow unique plugin names.
1049 # Only allow unique plugin names.
1050 if plugin.name in unique_names:
1050 if plugin.name in unique_names:
1051 msg = M(self, 'import_duplicate', state,
1051 msg = M(self, 'import_duplicate', state,
1052 loaded=unique_names[plugin.name],
1052 loaded=unique_names[plugin.name],
1053 next_to_load=plugin)
1053 next_to_load=plugin)
1054 raise formencode.Invalid(msg, value, state)
1054 raise formencode.Invalid(msg, value, state)
1055 unique_names[plugin.name] = plugin
1055 unique_names[plugin.name] = plugin
1056 return _validator
1056 return _validator
1057
1057
1058
1058
1059 def ValidPattern(localizer):
1059 def ValidPattern(localizer):
1060 _ = localizer
1060 _ = localizer
1061
1061
1062 class _validator(formencode.validators.FancyValidator):
1062 class _validator(formencode.validators.FancyValidator):
1063 messages = {
1063 messages = {
1064 'bad_format': _('Url must start with http or /'),
1064 'bad_format': _('Url must start with http or /'),
1065 }
1065 }
1066
1066
1067 def _convert_to_python(self, value, state):
1067 def _convert_to_python(self, value, state):
1068 patterns = []
1068 patterns = []
1069
1069
1070 prefix = 'new_pattern'
1070 prefix = 'new_pattern'
1071 for name, v in list(value.items()):
1071 for name, v in list(value.items()):
1072 pattern_name = '_'.join((prefix, 'pattern'))
1072 pattern_name = '_'.join((prefix, 'pattern'))
1073 if name.startswith(pattern_name):
1073 if name.startswith(pattern_name):
1074 new_item_id = name[len(pattern_name)+1:]
1074 new_item_id = name[len(pattern_name)+1:]
1075
1075
1076 def _field(name):
1076 def _field(name):
1077 return '{}_{}_{}'.format(prefix, name, new_item_id)
1077 return '{}_{}_{}'.format(prefix, name, new_item_id)
1078
1078
1079 values = {
1079 values = {
1080 'issuetracker_pat': value.get(_field('pattern')),
1080 'issuetracker_pat': value.get(_field('pattern')),
1081 'issuetracker_url': value.get(_field('url')),
1081 'issuetracker_url': value.get(_field('url')),
1082 'issuetracker_pref': value.get(_field('prefix')),
1082 'issuetracker_pref': value.get(_field('prefix')),
1083 'issuetracker_desc': value.get(_field('description'))
1083 'issuetracker_desc': value.get(_field('description'))
1084 }
1084 }
1085 new_uid = md5_safe(values['issuetracker_pat'])
1085 new_uid = md5_safe(values['issuetracker_pat'])
1086
1086
1087 has_required_fields = (
1087 has_required_fields = (
1088 values['issuetracker_pat']
1088 values['issuetracker_pat']
1089 and values['issuetracker_url'])
1089 and values['issuetracker_url'])
1090
1090
1091 if has_required_fields:
1091 if has_required_fields:
1092 # validate url that it starts with http or /
1092 # validate url that it starts with http or /
1093 # otherwise it can lead to JS injections
1093 # otherwise it can lead to JS injections
1094 # e.g specifig javascript:<malicios code>
1094 # e.g specifig javascript:<malicios code>
1095 if not values['issuetracker_url'].startswith(('http', '/')):
1095 if not values['issuetracker_url'].startswith(('http', '/')):
1096 raise formencode.Invalid(
1096 raise formencode.Invalid(
1097 self.message('bad_format', state),
1097 self.message('bad_format', state),
1098 value, state)
1098 value, state)
1099
1099
1100 settings = [
1100 settings = [
1101 ('_'.join((key, new_uid)), values[key], 'unicode')
1101 ('_'.join((key, new_uid)), values[key], 'unicode')
1102 for key in values]
1102 for key in values]
1103 patterns.append(settings)
1103 patterns.append(settings)
1104
1104
1105 value['patterns'] = patterns
1105 value['patterns'] = patterns
1106 delete_patterns = value.get('uid') or []
1106 delete_patterns = value.get('uid') or []
1107 if not isinstance(delete_patterns, (list, tuple)):
1107 if not isinstance(delete_patterns, (list, tuple)):
1108 delete_patterns = [delete_patterns]
1108 delete_patterns = [delete_patterns]
1109 value['delete_patterns'] = delete_patterns
1109 value['delete_patterns'] = delete_patterns
1110 return value
1110 return value
1111 return _validator
1111 return _validator
@@ -1,105 +1,105 b''
1 <%inherit file="base/root.mako"/>
1 <%inherit file="base/root.mako"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${_('Sign In')}
4 ${_('Sign In')}
5 %if c.rhodecode_name:
5 %if c.rhodecode_name:
6 &middot; ${h.branding(c.rhodecode_name)}
6 &middot; ${h.branding(c.rhodecode_name)}
7 %endif
7 %endif
8 </%def>
8 </%def>
9 <style>body{background-color:#eeeeee;}</style>
9 <style>body{background-color:#eeeeee;}</style>
10
10
11 <div class="loginbox">
11 <div class="loginbox">
12 <div class="header-account">
12 <div class="header-account">
13 <div id="header-inner" class="title">
13 <div id="header-inner" class="title">
14 <div id="logo">
14 <div id="logo">
15 <div class="logo-wrapper">
15 <div class="logo-wrapper">
16 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-60x60.png')}" alt="RhodeCode"/></a>
16 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-60x60.png')}" alt="RhodeCode"/></a>
17 </div>
17 </div>
18 % if c.rhodecode_name:
18 % if c.rhodecode_name:
19 <div class="branding">
19 <div class="branding">
20 <a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a>
20 <a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a>
21 </div>
21 </div>
22 % endif
22 % endif
23 </div>
23 </div>
24 </div>
24 </div>
25 </div>
25 </div>
26
26
27 <div class="loginwrapper">
27 <div class="loginwrapper">
28 <rhodecode-toast id="notifications"></rhodecode-toast>
28 <rhodecode-toast id="notifications"></rhodecode-toast>
29
29
30 <div class="auth-image-wrapper">
30 <div class="auth-image-wrapper">
31 <img class="sign-in-image" src="${h.asset('images/sign-in.png')}" alt="RhodeCode"/>
31 <img class="sign-in-image" src="${h.asset('images/sign-in.png')}" alt="RhodeCode"/>
32 </div>
32 </div>
33
33
34 <div id="login">
34 <div id="login">
35 <%block name="above_login_button" />
35 <%block name="above_login_button" />
36 <!-- login -->
36 <!-- login -->
37 <div class="sign-in-title">
37 <div class="sign-in-title">
38 <h1>${_('Sign In using username/password')}</h1>
38 <h1>${_('Sign In using credentials')}</h1>
39 </div>
39 </div>
40 <div class="inner form">
40 <div class="inner form">
41 ${h.form(request.route_path('login', _query={'came_from': c.came_from}), needs_csrf_token=False)}
41 ${h.form(request.route_path('login', _query={'came_from': c.came_from}), needs_csrf_token=False)}
42
42
43 <label for="username">${_('Username')}:</label>
43 <label for="username">${_('Username or email address')}:</label>
44 ${h.text('username', class_='focus', value=defaults.get('username'))}
44 ${h.text('username', class_='focus', value=defaults.get('username'))}
45 %if 'username' in errors:
45 %if 'username' in errors:
46 <span class="error-message">${errors.get('username')}</span>
46 <span class="error-message">${errors.get('username')}</span>
47 <br />
47 <br />
48 %endif
48 %endif
49
49
50 <label for="password">${_('Password')}:
50 <label for="password">${_('Password')}:
51 %if h.HasPermissionAny('hg.password_reset.enabled')():
51 %if h.HasPermissionAny('hg.password_reset.enabled')():
52 <div class="pull-right">${h.link_to(_('Forgot your password?'), h.route_path('reset_password'), class_='pwd_reset', tabindex="-1")}</div>
52 <div class="pull-right">${h.link_to(_('Forgot your password?'), h.route_path('reset_password'), class_='pwd_reset', tabindex="-1")}</div>
53 %endif
53 %endif
54
54
55 </label>
55 </label>
56 ${h.password('password', class_='focus')}
56 ${h.password('password', class_='focus')}
57 %if 'password' in errors:
57 %if 'password' in errors:
58 <span class="error-message">${errors.get('password')}</span>
58 <span class="error-message">${errors.get('password')}</span>
59 <br />
59 <br />
60 %endif
60 %endif
61
61
62 ${h.checkbox('remember', value=True, checked=defaults.get('remember'))}
62 ${h.checkbox('remember', value=True, checked=defaults.get('remember'))}
63 <% timeout = request.registry.settings.get('beaker.session.timeout', '0') %>
63 <% timeout = request.registry.settings.get('beaker.session.timeout', '0') %>
64 % if timeout == '0':
64 % if timeout == '0':
65 <% remember_label = _('Remember my indefinitely') %>
65 <% remember_label = _('Remember my indefinitely') %>
66 % else:
66 % else:
67 <% remember_label = _('Remember me for {}').format(h.age_from_seconds(timeout)) %>
67 <% remember_label = _('Remember me for {}').format(h.age_from_seconds(timeout)) %>
68 % endif
68 % endif
69 <label class="checkbox" for="remember">${remember_label}</label>
69 <label class="checkbox" for="remember">${remember_label}</label>
70
70
71 <p class="links">
71 <p class="links">
72 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
72 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
73 ${h.link_to(_("Create a new account."), request.route_path('register'), class_='new_account')}
73 ${h.link_to(_("Create a new account."), request.route_path('register'), class_='new_account')}
74 %endif
74 %endif
75 </p>
75 </p>
76
76
77 %if not h.HasPermissionAny('hg.password_reset.enabled')():
77 %if not h.HasPermissionAny('hg.password_reset.enabled')():
78 ## password reset hidden or disabled.
78 ## password reset hidden or disabled.
79 <p class="help-block">
79 <p class="help-block">
80 ${_('Password reset is disabled.')} <br/>
80 ${_('Password reset is disabled.')} <br/>
81 ${_('Please contact ')}
81 ${_('Please contact ')}
82 % if c.visual.rhodecode_support_url:
82 % if c.visual.rhodecode_support_url:
83 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
83 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
84 ${_('or')}
84 ${_('or')}
85 % endif
85 % endif
86 ${_('an administrator if you need help.')}
86 ${_('an administrator if you need help.')}
87 </p>
87 </p>
88 %endif
88 %endif
89
89
90 ${h.submit('sign_in', _('Sign In'), class_="btn sign-in", title=_('Sign in to {}').format(c.rhodecode_edition))}
90 ${h.submit('sign_in', _('Sign In'), class_="btn sign-in", title=_('Sign in to {}').format(c.rhodecode_edition))}
91
91
92 ${h.end_form()}
92 ${h.end_form()}
93 <script type="text/javascript">
93 <script type="text/javascript">
94 $(document).ready(function(){
94 $(document).ready(function(){
95 $('#username').focus();
95 $('#username').focus();
96 })
96 })
97 </script>
97 </script>
98
98
99 </div>
99 </div>
100 <!-- end login -->
100 <!-- end login -->
101
101
102 <%block name="below_login_button" />
102 <%block name="below_login_button" />
103 </div>
103 </div>
104 </div>
104 </div>
105 </div>
105 </div>
General Comments 0
You need to be logged in to leave comments. Login now