##// END OF EJS Templates
users: make AuthUser propert a method, and allow override of params.
marcink -
r1997:61825b68 default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,129 +1,129 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import pytest
23 23
24 24 from rhodecode.model.repo import RepoModel
25 25 from rhodecode.api.tests.utils import (
26 26 build_data, api_call, assert_ok, assert_error, jsonify)
27 27 from rhodecode.model.db import User
28 28
29 29
30 30 @pytest.mark.usefixtures("testuser_api", "app")
31 31 class TestGetRepos(object):
32 32 def test_api_get_repos(self):
33 33 id_, params = build_data(self.apikey, 'get_repos')
34 34 response = api_call(self.app, params)
35 35
36 36 result = []
37 37 for repo in RepoModel().get_all():
38 38 result.append(repo.get_api_data(include_secrets=True))
39 39 ret = jsonify(result)
40 40
41 41 expected = ret
42 42 assert_ok(id_, expected, given=response.body)
43 43
44 44 def test_api_get_repos_only_toplevel(self, user_util):
45 45 repo_group = user_util.create_repo_group(auto_cleanup=True)
46 46 user_util.create_repo(parent=repo_group)
47 47
48 48 id_, params = build_data(self.apikey, 'get_repos', traverse=0)
49 49 response = api_call(self.app, params)
50 50
51 51 result = []
52 52 for repo in RepoModel().get_repos_for_root(root=None):
53 53 result.append(repo.get_api_data(include_secrets=True))
54 54 expected = jsonify(result)
55 55
56 56 assert_ok(id_, expected, given=response.body)
57 57
58 58 def test_api_get_repos_with_wrong_root(self):
59 59 id_, params = build_data(self.apikey, 'get_repos', root='abracadabra')
60 60 response = api_call(self.app, params)
61 61
62 62 expected = 'Root repository group `abracadabra` does not exist'
63 63 assert_error(id_, expected, given=response.body)
64 64
65 65 def test_api_get_repos_with_root(self, user_util):
66 66 repo_group = user_util.create_repo_group(auto_cleanup=True)
67 67 repo_group_name = repo_group.group_name
68 68
69 69 user_util.create_repo(parent=repo_group)
70 70 user_util.create_repo(parent=repo_group)
71 71
72 72 # nested, should not show up
73 73 user_util._test_name = '{}/'.format(repo_group_name)
74 74 sub_repo_group = user_util.create_repo_group(auto_cleanup=True)
75 75 user_util.create_repo(parent=sub_repo_group)
76 76
77 77 id_, params = build_data(self.apikey, 'get_repos',
78 78 root=repo_group_name, traverse=0)
79 79 response = api_call(self.app, params)
80 80
81 81 result = []
82 82 for repo in RepoModel().get_repos_for_root(repo_group):
83 83 result.append(repo.get_api_data(include_secrets=True))
84 84
85 85 assert len(result) == 2
86 86 expected = jsonify(result)
87 87 assert_ok(id_, expected, given=response.body)
88 88
89 89 def test_api_get_repos_with_root_and_traverse(self, user_util):
90 90 repo_group = user_util.create_repo_group(auto_cleanup=True)
91 91 repo_group_name = repo_group.group_name
92 92
93 93 user_util.create_repo(parent=repo_group)
94 94 user_util.create_repo(parent=repo_group)
95 95
96 96 # nested, should not show up
97 97 user_util._test_name = '{}/'.format(repo_group_name)
98 98 sub_repo_group = user_util.create_repo_group(auto_cleanup=True)
99 99 user_util.create_repo(parent=sub_repo_group)
100 100
101 101 id_, params = build_data(self.apikey, 'get_repos',
102 102 root=repo_group_name, traverse=1)
103 103 response = api_call(self.app, params)
104 104
105 105 result = []
106 106 for repo in RepoModel().get_repos_for_root(
107 107 repo_group_name, traverse=True):
108 108 result.append(repo.get_api_data(include_secrets=True))
109 109
110 110 assert len(result) == 3
111 111 expected = jsonify(result)
112 112 assert_ok(id_, expected, given=response.body)
113 113
114 114 def test_api_get_repos_non_admin(self):
115 115 id_, params = build_data(self.apikey_regular, 'get_repos')
116 116 response = api_call(self.app, params)
117 117
118 118 user = User.get_by_username(self.TEST_USER_LOGIN)
119 allowed_repos = user.AuthUser.permissions['repositories']
119 allowed_repos = user.AuthUser().permissions['repositories']
120 120
121 121 result = []
122 122 for repo in RepoModel().get_all():
123 123 perm = allowed_repos[repo.repo_name]
124 124 if perm in ['repository.read', 'repository.write', 'repository.admin']:
125 125 result.append(repo.get_api_data())
126 126 ret = jsonify(result)
127 127
128 128 expected = ret
129 129 assert_ok(id_, expected, given=response.body)
@@ -1,369 +1,369 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import re
22 22 import logging
23 23 import formencode
24 24 from pyramid.interfaces import IRoutesMapper
25 25
26 26 from pyramid.view import view_config
27 27 from pyramid.httpexceptions import HTTPFound
28 28 from pyramid.renderers import render
29 29 from pyramid.response import Response
30 30
31 31 from rhodecode.apps._base import BaseAppView
32 32
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.lib.auth import (
35 35 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
36 36 from rhodecode.lib.utils2 import aslist
37 37 from rhodecode.model.db import User, UserIpMap
38 38 from rhodecode.model.forms import (
39 39 ApplicationPermissionsForm, ObjectPermissionsForm, UserPermissionsForm)
40 40 from rhodecode.model.meta import Session
41 41 from rhodecode.model.permission import PermissionModel
42 42 from rhodecode.model.settings import SettingsModel
43 43
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 class AdminPermissionsView(BaseAppView):
49 49 def load_default_context(self):
50 50 c = self._get_local_tmpl_context()
51 51
52 52 self._register_global_c(c)
53 53 PermissionModel().set_global_permission_choices(
54 54 c, gettext_translator=self.request.translate)
55 55 return c
56 56
57 57 @LoginRequired()
58 58 @HasPermissionAllDecorator('hg.admin')
59 59 @view_config(
60 60 route_name='admin_permissions_application', request_method='GET',
61 61 renderer='rhodecode:templates/admin/permissions/permissions.mako')
62 62 def permissions_application(self):
63 63 c = self.load_default_context()
64 64 c.active = 'application'
65 65
66 66 c.user = User.get_default_user(refresh=True)
67 67
68 68 app_settings = SettingsModel().get_all_settings()
69 69 defaults = {
70 70 'anonymous': c.user.active,
71 71 'default_register_message': app_settings.get(
72 72 'rhodecode_register_message')
73 73 }
74 74 defaults.update(c.user.get_default_perms())
75 75
76 76 data = render('rhodecode:templates/admin/permissions/permissions.mako',
77 77 self._get_template_context(c), self.request)
78 78 html = formencode.htmlfill.render(
79 79 data,
80 80 defaults=defaults,
81 81 encoding="UTF-8",
82 82 force_defaults=False
83 83 )
84 84 return Response(html)
85 85
86 86 @LoginRequired()
87 87 @HasPermissionAllDecorator('hg.admin')
88 88 @CSRFRequired()
89 89 @view_config(
90 90 route_name='admin_permissions_application_update', request_method='POST',
91 91 renderer='rhodecode:templates/admin/permissions/permissions.mako')
92 92 def permissions_application_update(self):
93 93 _ = self.request.translate
94 94 c = self.load_default_context()
95 95 c.active = 'application'
96 96
97 97 _form = ApplicationPermissionsForm(
98 98 [x[0] for x in c.register_choices],
99 99 [x[0] for x in c.password_reset_choices],
100 100 [x[0] for x in c.extern_activate_choices])()
101 101
102 102 try:
103 103 form_result = _form.to_python(dict(self.request.POST))
104 104 form_result.update({'perm_user_name': User.DEFAULT_USER})
105 105 PermissionModel().update_application_permissions(form_result)
106 106
107 107 settings = [
108 108 ('register_message', 'default_register_message'),
109 109 ]
110 110 for setting, form_key in settings:
111 111 sett = SettingsModel().create_or_update_setting(
112 112 setting, form_result[form_key])
113 113 Session().add(sett)
114 114
115 115 Session().commit()
116 116 h.flash(_('Application permissions updated successfully'),
117 117 category='success')
118 118
119 119 except formencode.Invalid as errors:
120 120 defaults = errors.value
121 121
122 122 data = render(
123 123 'rhodecode:templates/admin/permissions/permissions.mako',
124 124 self._get_template_context(c), self.request)
125 125 html = formencode.htmlfill.render(
126 126 data,
127 127 defaults=defaults,
128 128 errors=errors.error_dict or {},
129 129 prefix_error=False,
130 130 encoding="UTF-8",
131 131 force_defaults=False
132 132 )
133 133 return Response(html)
134 134
135 135 except Exception:
136 136 log.exception("Exception during update of permissions")
137 137 h.flash(_('Error occurred during update of permissions'),
138 138 category='error')
139 139
140 140 raise HTTPFound(h.route_path('admin_permissions_application'))
141 141
142 142 @LoginRequired()
143 143 @HasPermissionAllDecorator('hg.admin')
144 144 @view_config(
145 145 route_name='admin_permissions_object', request_method='GET',
146 146 renderer='rhodecode:templates/admin/permissions/permissions.mako')
147 147 def permissions_objects(self):
148 148 c = self.load_default_context()
149 149 c.active = 'objects'
150 150
151 151 c.user = User.get_default_user(refresh=True)
152 152 defaults = {}
153 153 defaults.update(c.user.get_default_perms())
154 154
155 155 data = render(
156 156 'rhodecode:templates/admin/permissions/permissions.mako',
157 157 self._get_template_context(c), self.request)
158 158 html = formencode.htmlfill.render(
159 159 data,
160 160 defaults=defaults,
161 161 encoding="UTF-8",
162 162 force_defaults=False
163 163 )
164 164 return Response(html)
165 165
166 166 @LoginRequired()
167 167 @HasPermissionAllDecorator('hg.admin')
168 168 @CSRFRequired()
169 169 @view_config(
170 170 route_name='admin_permissions_object_update', request_method='POST',
171 171 renderer='rhodecode:templates/admin/permissions/permissions.mako')
172 172 def permissions_objects_update(self):
173 173 _ = self.request.translate
174 174 c = self.load_default_context()
175 175 c.active = 'objects'
176 176
177 177 _form = ObjectPermissionsForm(
178 178 [x[0] for x in c.repo_perms_choices],
179 179 [x[0] for x in c.group_perms_choices],
180 180 [x[0] for x in c.user_group_perms_choices])()
181 181
182 182 try:
183 183 form_result = _form.to_python(dict(self.request.POST))
184 184 form_result.update({'perm_user_name': User.DEFAULT_USER})
185 185 PermissionModel().update_object_permissions(form_result)
186 186
187 187 Session().commit()
188 188 h.flash(_('Object permissions updated successfully'),
189 189 category='success')
190 190
191 191 except formencode.Invalid as errors:
192 192 defaults = errors.value
193 193
194 194 data = render(
195 195 'rhodecode:templates/admin/permissions/permissions.mako',
196 196 self._get_template_context(c), self.request)
197 197 html = formencode.htmlfill.render(
198 198 data,
199 199 defaults=defaults,
200 200 errors=errors.error_dict or {},
201 201 prefix_error=False,
202 202 encoding="UTF-8",
203 203 force_defaults=False
204 204 )
205 205 return Response(html)
206 206 except Exception:
207 207 log.exception("Exception during update of permissions")
208 208 h.flash(_('Error occurred during update of permissions'),
209 209 category='error')
210 210
211 211 raise HTTPFound(h.route_path('admin_permissions_object'))
212 212
213 213 @LoginRequired()
214 214 @HasPermissionAllDecorator('hg.admin')
215 215 @view_config(
216 216 route_name='admin_permissions_global', request_method='GET',
217 217 renderer='rhodecode:templates/admin/permissions/permissions.mako')
218 218 def permissions_global(self):
219 219 c = self.load_default_context()
220 220 c.active = 'global'
221 221
222 222 c.user = User.get_default_user(refresh=True)
223 223 defaults = {}
224 224 defaults.update(c.user.get_default_perms())
225 225
226 226 data = render(
227 227 'rhodecode:templates/admin/permissions/permissions.mako',
228 228 self._get_template_context(c), self.request)
229 229 html = formencode.htmlfill.render(
230 230 data,
231 231 defaults=defaults,
232 232 encoding="UTF-8",
233 233 force_defaults=False
234 234 )
235 235 return Response(html)
236 236
237 237 @LoginRequired()
238 238 @HasPermissionAllDecorator('hg.admin')
239 239 @CSRFRequired()
240 240 @view_config(
241 241 route_name='admin_permissions_global_update', request_method='POST',
242 242 renderer='rhodecode:templates/admin/permissions/permissions.mako')
243 243 def permissions_global_update(self):
244 244 _ = self.request.translate
245 245 c = self.load_default_context()
246 246 c.active = 'global'
247 247
248 248 _form = UserPermissionsForm(
249 249 [x[0] for x in c.repo_create_choices],
250 250 [x[0] for x in c.repo_create_on_write_choices],
251 251 [x[0] for x in c.repo_group_create_choices],
252 252 [x[0] for x in c.user_group_create_choices],
253 253 [x[0] for x in c.fork_choices],
254 254 [x[0] for x in c.inherit_default_permission_choices])()
255 255
256 256 try:
257 257 form_result = _form.to_python(dict(self.request.POST))
258 258 form_result.update({'perm_user_name': User.DEFAULT_USER})
259 259 PermissionModel().update_user_permissions(form_result)
260 260
261 261 Session().commit()
262 262 h.flash(_('Global permissions updated successfully'),
263 263 category='success')
264 264
265 265 except formencode.Invalid as errors:
266 266 defaults = errors.value
267 267
268 268 data = render(
269 269 'rhodecode:templates/admin/permissions/permissions.mako',
270 270 self._get_template_context(c), self.request)
271 271 html = formencode.htmlfill.render(
272 272 data,
273 273 defaults=defaults,
274 274 errors=errors.error_dict or {},
275 275 prefix_error=False,
276 276 encoding="UTF-8",
277 277 force_defaults=False
278 278 )
279 279 return Response(html)
280 280 except Exception:
281 281 log.exception("Exception during update of permissions")
282 282 h.flash(_('Error occurred during update of permissions'),
283 283 category='error')
284 284
285 285 raise HTTPFound(h.route_path('admin_permissions_global'))
286 286
287 287 @LoginRequired()
288 288 @HasPermissionAllDecorator('hg.admin')
289 289 @view_config(
290 290 route_name='admin_permissions_ips', request_method='GET',
291 291 renderer='rhodecode:templates/admin/permissions/permissions.mako')
292 292 def permissions_ips(self):
293 293 c = self.load_default_context()
294 294 c.active = 'ips'
295 295
296 296 c.user = User.get_default_user(refresh=True)
297 297 c.user_ip_map = (
298 298 UserIpMap.query().filter(UserIpMap.user == c.user).all())
299 299
300 300 return self._get_template_context(c)
301 301
302 302 @LoginRequired()
303 303 @HasPermissionAllDecorator('hg.admin')
304 304 @view_config(
305 305 route_name='admin_permissions_overview', request_method='GET',
306 306 renderer='rhodecode:templates/admin/permissions/permissions.mako')
307 307 def permissions_overview(self):
308 308 c = self.load_default_context()
309 309 c.active = 'perms'
310 310
311 311 c.user = User.get_default_user(refresh=True)
312 c.perm_user = c.user.AuthUser
312 c.perm_user = c.user.AuthUser()
313 313 return self._get_template_context(c)
314 314
315 315 @LoginRequired()
316 316 @HasPermissionAllDecorator('hg.admin')
317 317 @view_config(
318 318 route_name='admin_permissions_auth_token_access', request_method='GET',
319 319 renderer='rhodecode:templates/admin/permissions/permissions.mako')
320 320 def auth_token_access(self):
321 321 from rhodecode import CONFIG
322 322
323 323 c = self.load_default_context()
324 324 c.active = 'auth_token_access'
325 325
326 326 c.user = User.get_default_user(refresh=True)
327 c.perm_user = c.user.AuthUser
327 c.perm_user = c.user.AuthUser()
328 328
329 329 mapper = self.request.registry.queryUtility(IRoutesMapper)
330 330 c.view_data = []
331 331
332 332 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
333 333 introspector = self.request.registry.introspector
334 334
335 335 view_intr = {}
336 336 for view_data in introspector.get_category('views'):
337 337 intr = view_data['introspectable']
338 338
339 339 if 'route_name' in intr and intr['attr']:
340 340 view_intr[intr['route_name']] = '{}:{}'.format(
341 341 str(intr['derived_callable'].func_name), intr['attr']
342 342 )
343 343
344 344 c.whitelist_key = 'api_access_controllers_whitelist'
345 345 c.whitelist_file = CONFIG.get('__file__')
346 346 whitelist_views = aslist(
347 347 CONFIG.get(c.whitelist_key), sep=',')
348 348
349 349 for route_info in mapper.get_routes():
350 350 if not route_info.name.startswith('__'):
351 351 routepath = route_info.pattern
352 352
353 353 def replace(matchobj):
354 354 if matchobj.group(1):
355 355 return "{%s}" % matchobj.group(1).split(':')[0]
356 356 else:
357 357 return "{%s}" % matchobj.group(2)
358 358
359 359 routepath = _argument_prog.sub(replace, routepath)
360 360
361 361 if not routepath.startswith('/'):
362 362 routepath = '/' + routepath
363 363
364 364 view_fqn = view_intr.get(route_info.name, 'NOT AVAILABLE')
365 365 active = view_fqn in whitelist_views
366 366 c.view_data.append((route_info.name, view_fqn, routepath, active))
367 367
368 368 c.whitelist_views = whitelist_views
369 369 return self._get_template_context(c)
@@ -1,133 +1,133 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import mock
23 23 import pytest
24 24
25 25 from rhodecode.apps.login.views import LoginView, CaptchaData
26 26 from rhodecode.config.routing import ADMIN_PREFIX
27 27 from rhodecode.lib.utils2 import AttributeDict
28 28 from rhodecode.model.settings import SettingsModel
29 29 from rhodecode.tests.utils import AssertResponse
30 30
31 31
32 32 class RhodeCodeSetting(object):
33 33 def __init__(self, name, value):
34 34 self.name = name
35 35 self.value = value
36 36
37 37 def __enter__(self):
38 38 from rhodecode.model.settings import SettingsModel
39 39 model = SettingsModel()
40 40 self.old_setting = model.get_setting_by_name(self.name)
41 41 model.create_or_update_setting(name=self.name, val=self.value)
42 42 return self
43 43
44 44 def __exit__(self, exc_type, exc_val, exc_tb):
45 45 model = SettingsModel()
46 46 if self.old_setting:
47 47 model.create_or_update_setting(
48 48 name=self.name, val=self.old_setting.app_settings_value)
49 49 else:
50 50 model.create_or_update_setting(name=self.name)
51 51
52 52
53 53 class TestRegisterCaptcha(object):
54 54
55 55 @pytest.mark.parametrize('private_key, public_key, expected', [
56 56 ('', '', CaptchaData(False, '', '')),
57 57 ('', 'pubkey', CaptchaData(False, '', 'pubkey')),
58 58 ('privkey', '', CaptchaData(True, 'privkey', '')),
59 59 ('privkey', 'pubkey', CaptchaData(True, 'privkey', 'pubkey')),
60 60 ])
61 61 def test_get_captcha_data(self, private_key, public_key, expected, db,
62 62 request_stub, user_util):
63 request_stub.user = user_util.create_user().AuthUser
63 request_stub.user = user_util.create_user().AuthUser()
64 64 request_stub.matched_route = AttributeDict({'name': 'login'})
65 65 login_view = LoginView(mock.Mock(), request_stub)
66 66
67 67 with RhodeCodeSetting('captcha_private_key', private_key):
68 68 with RhodeCodeSetting('captcha_public_key', public_key):
69 69 captcha = login_view._get_captcha_data()
70 70 assert captcha == expected
71 71
72 72 @pytest.mark.parametrize('active', [False, True])
73 73 @mock.patch.object(LoginView, '_get_captcha_data')
74 74 def test_private_key_does_not_leak_to_html(
75 75 self, m_get_captcha_data, active, app):
76 76 captcha = CaptchaData(
77 77 active=active, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
78 78 m_get_captcha_data.return_value = captcha
79 79
80 80 response = app.get(ADMIN_PREFIX + '/register')
81 81 assert 'PRIVATE_KEY' not in response
82 82
83 83 @pytest.mark.parametrize('active', [False, True])
84 84 @mock.patch.object(LoginView, '_get_captcha_data')
85 85 def test_register_view_renders_captcha(
86 86 self, m_get_captcha_data, active, app):
87 87 captcha = CaptchaData(
88 88 active=active, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
89 89 m_get_captcha_data.return_value = captcha
90 90
91 91 response = app.get(ADMIN_PREFIX + '/register')
92 92
93 93 assertr = AssertResponse(response)
94 94 if active:
95 95 assertr.one_element_exists('#recaptcha_field')
96 96 else:
97 97 assertr.no_element_exists('#recaptcha_field')
98 98
99 99 @pytest.mark.parametrize('valid', [False, True])
100 100 @mock.patch('rhodecode.apps.login.views.submit')
101 101 @mock.patch.object(LoginView, '_get_captcha_data')
102 102 def test_register_with_active_captcha(
103 103 self, m_get_captcha_data, m_submit, valid, app, csrf_token):
104 104 captcha = CaptchaData(
105 105 active=True, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
106 106 m_get_captcha_data.return_value = captcha
107 107 m_response = mock.Mock()
108 108 m_response.is_valid = valid
109 109 m_submit.return_value = m_response
110 110
111 111 params = {
112 112 'csrf_token': csrf_token,
113 113 'email': 'pytest@example.com',
114 114 'firstname': 'pytest-firstname',
115 115 'lastname': 'pytest-lastname',
116 116 'password': 'secret',
117 117 'password_confirmation': 'secret',
118 118 'username': 'pytest',
119 119 }
120 120 response = app.post(ADMIN_PREFIX + '/register', params=params)
121 121
122 122 if valid:
123 123 # If we provided a valid captcha input we expect a successful
124 124 # registration and redirect to the login page.
125 125 assert response.status_int == 302
126 126 assert 'location' in response.headers
127 127 assert ADMIN_PREFIX + '/login' in response.headers['location']
128 128 else:
129 129 # If captche input is invalid we expect to stay on the registration
130 130 # page with an error message displayed.
131 131 assertr = AssertResponse(response)
132 132 assert response.status_int == 200
133 133 assertr.one_element_exists('#recaptcha_field ~ span.error-message')
@@ -1,425 +1,425 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import time
22 22 import collections
23 23 import datetime
24 24 import formencode
25 25 import logging
26 26 import urlparse
27 27
28 28 from pyramid.httpexceptions import HTTPFound
29 29 from pyramid.view import view_config
30 30 from recaptcha.client.captcha import submit
31 31
32 32 from rhodecode.apps._base import BaseAppView
33 33 from rhodecode.authentication.base import authenticate, HTTP_TYPE
34 34 from rhodecode.events import UserRegistered
35 35 from rhodecode.lib import helpers as h
36 36 from rhodecode.lib import audit_logger
37 37 from rhodecode.lib.auth import (
38 38 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
39 39 from rhodecode.lib.base import get_ip_addr
40 40 from rhodecode.lib.exceptions import UserCreationError
41 41 from rhodecode.lib.utils2 import safe_str
42 42 from rhodecode.model.db import User, UserApiKeys
43 43 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
44 44 from rhodecode.model.meta import Session
45 45 from rhodecode.model.auth_token import AuthTokenModel
46 46 from rhodecode.model.settings import SettingsModel
47 47 from rhodecode.model.user import UserModel
48 48 from rhodecode.translation import _
49 49
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53 CaptchaData = collections.namedtuple(
54 54 'CaptchaData', 'active, private_key, public_key')
55 55
56 56
57 57 def _store_user_in_session(session, username, remember=False):
58 58 user = User.get_by_username(username, case_insensitive=True)
59 59 auth_user = AuthUser(user.user_id)
60 60 auth_user.set_authenticated()
61 61 cs = auth_user.get_cookie_store()
62 62 session['rhodecode_user'] = cs
63 63 user.update_lastlogin()
64 64 Session().commit()
65 65
66 66 # If they want to be remembered, update the cookie
67 67 if remember:
68 68 _year = (datetime.datetime.now() +
69 69 datetime.timedelta(seconds=60 * 60 * 24 * 365))
70 70 session._set_cookie_expires(_year)
71 71
72 72 session.save()
73 73
74 74 safe_cs = cs.copy()
75 75 safe_cs['password'] = '****'
76 76 log.info('user %s is now authenticated and stored in '
77 77 'session, session attrs %s', username, safe_cs)
78 78
79 79 # dumps session attrs back to cookie
80 80 session._update_cookie_out()
81 81 # we set new cookie
82 82 headers = None
83 83 if session.request['set_cookie']:
84 84 # send set-cookie headers back to response to update cookie
85 85 headers = [('Set-Cookie', session.request['cookie_out'])]
86 86 return headers
87 87
88 88
89 89 def get_came_from(request):
90 90 came_from = safe_str(request.GET.get('came_from', ''))
91 91 parsed = urlparse.urlparse(came_from)
92 92 allowed_schemes = ['http', 'https']
93 93 default_came_from = h.route_path('home')
94 94 if parsed.scheme and parsed.scheme not in allowed_schemes:
95 95 log.error('Suspicious URL scheme detected %s for url %s' %
96 96 (parsed.scheme, parsed))
97 97 came_from = default_came_from
98 98 elif parsed.netloc and request.host != parsed.netloc:
99 99 log.error('Suspicious NETLOC detected %s for url %s server url '
100 100 'is: %s' % (parsed.netloc, parsed, request.host))
101 101 came_from = default_came_from
102 102 elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
103 103 log.error('Header injection detected `%s` for url %s server url ' %
104 104 (parsed.path, parsed))
105 105 came_from = default_came_from
106 106
107 107 return came_from or default_came_from
108 108
109 109
110 110 class LoginView(BaseAppView):
111 111
112 112 def load_default_context(self):
113 113 c = self._get_local_tmpl_context()
114 114 c.came_from = get_came_from(self.request)
115 115 self._register_global_c(c)
116 116 return c
117 117
118 118 def _get_captcha_data(self):
119 119 settings = SettingsModel().get_all_settings()
120 120 private_key = settings.get('rhodecode_captcha_private_key')
121 121 public_key = settings.get('rhodecode_captcha_public_key')
122 122 active = bool(private_key)
123 123 return CaptchaData(
124 124 active=active, private_key=private_key, public_key=public_key)
125 125
126 126 @view_config(
127 127 route_name='login', request_method='GET',
128 128 renderer='rhodecode:templates/login.mako')
129 129 def login(self):
130 130 c = self.load_default_context()
131 131 auth_user = self._rhodecode_user
132 132
133 133 # redirect if already logged in
134 134 if (auth_user.is_authenticated and
135 135 not auth_user.is_default and auth_user.ip_allowed):
136 136 raise HTTPFound(c.came_from)
137 137
138 138 # check if we use headers plugin, and try to login using it.
139 139 try:
140 140 log.debug('Running PRE-AUTH for headers based authentication')
141 141 auth_info = authenticate(
142 142 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
143 143 if auth_info:
144 144 headers = _store_user_in_session(
145 145 self.session, auth_info.get('username'))
146 146 raise HTTPFound(c.came_from, headers=headers)
147 147 except UserCreationError as e:
148 148 log.error(e)
149 149 self.session.flash(e, queue='error')
150 150
151 151 return self._get_template_context(c)
152 152
153 153 @view_config(
154 154 route_name='login', request_method='POST',
155 155 renderer='rhodecode:templates/login.mako')
156 156 def login_post(self):
157 157 c = self.load_default_context()
158 158
159 159 login_form = LoginForm()()
160 160
161 161 try:
162 162 self.session.invalidate()
163 163 form_result = login_form.to_python(self.request.params)
164 164 # form checks for username/password, now we're authenticated
165 165 headers = _store_user_in_session(
166 166 self.session,
167 167 username=form_result['username'],
168 168 remember=form_result['remember'])
169 169 log.debug('Redirecting to "%s" after login.', c.came_from)
170 170
171 171 audit_user = audit_logger.UserWrap(
172 172 username=self.request.params.get('username'),
173 173 ip_addr=self.request.remote_addr)
174 174 action_data = {'user_agent': self.request.user_agent}
175 175 audit_logger.store_web(
176 176 'user.login.success', action_data=action_data,
177 177 user=audit_user, commit=True)
178 178
179 179 raise HTTPFound(c.came_from, headers=headers)
180 180 except formencode.Invalid as errors:
181 181 defaults = errors.value
182 182 # remove password from filling in form again
183 183 defaults.pop('password', None)
184 184 render_ctx = self._get_template_context(c)
185 185 render_ctx.update({
186 186 'errors': errors.error_dict,
187 187 'defaults': defaults,
188 188 })
189 189
190 190 audit_user = audit_logger.UserWrap(
191 191 username=self.request.params.get('username'),
192 192 ip_addr=self.request.remote_addr)
193 193 action_data = {'user_agent': self.request.user_agent}
194 194 audit_logger.store_web(
195 195 'user.login.failure', action_data=action_data,
196 196 user=audit_user, commit=True)
197 197 return render_ctx
198 198
199 199 except UserCreationError as e:
200 200 # headers auth or other auth functions that create users on
201 201 # the fly can throw this exception signaling that there's issue
202 202 # with user creation, explanation should be provided in
203 203 # Exception itself
204 204 self.session.flash(e, queue='error')
205 205 return self._get_template_context(c)
206 206
207 207 @CSRFRequired()
208 208 @view_config(route_name='logout', request_method='POST')
209 209 def logout(self):
210 210 auth_user = self._rhodecode_user
211 211 log.info('Deleting session for user: `%s`', auth_user)
212 212
213 213 action_data = {'user_agent': self.request.user_agent}
214 214 audit_logger.store_web(
215 215 'user.logout', action_data=action_data,
216 216 user=auth_user, commit=True)
217 217 self.session.delete()
218 218 return HTTPFound(h.route_path('home'))
219 219
220 220 @HasPermissionAnyDecorator(
221 221 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
222 222 @view_config(
223 223 route_name='register', request_method='GET',
224 224 renderer='rhodecode:templates/register.mako',)
225 225 def register(self, defaults=None, errors=None):
226 226 c = self.load_default_context()
227 227 defaults = defaults or {}
228 228 errors = errors or {}
229 229
230 230 settings = SettingsModel().get_all_settings()
231 231 register_message = settings.get('rhodecode_register_message') or ''
232 232 captcha = self._get_captcha_data()
233 233 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
234 .AuthUser.permissions['global']
234 .AuthUser().permissions['global']
235 235
236 236 render_ctx = self._get_template_context(c)
237 237 render_ctx.update({
238 238 'defaults': defaults,
239 239 'errors': errors,
240 240 'auto_active': auto_active,
241 241 'captcha_active': captcha.active,
242 242 'captcha_public_key': captcha.public_key,
243 243 'register_message': register_message,
244 244 })
245 245 return render_ctx
246 246
247 247 @HasPermissionAnyDecorator(
248 248 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
249 249 @view_config(
250 250 route_name='register', request_method='POST',
251 251 renderer='rhodecode:templates/register.mako')
252 252 def register_post(self):
253 253 captcha = self._get_captcha_data()
254 254 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
255 .AuthUser.permissions['global']
255 .AuthUser().permissions['global']
256 256
257 257 register_form = RegisterForm()()
258 258 try:
259 259 form_result = register_form.to_python(self.request.params)
260 260 form_result['active'] = auto_active
261 261
262 262 if captcha.active:
263 263 response = submit(
264 264 self.request.params.get('recaptcha_challenge_field'),
265 265 self.request.params.get('recaptcha_response_field'),
266 266 private_key=captcha.private_key,
267 267 remoteip=get_ip_addr(self.request.environ))
268 268 if not response.is_valid:
269 269 _value = form_result
270 270 _msg = _('Bad captcha')
271 271 error_dict = {'recaptcha_field': _msg}
272 272 raise formencode.Invalid(_msg, _value, None,
273 273 error_dict=error_dict)
274 274
275 275 new_user = UserModel().create_registration(form_result)
276 276 event = UserRegistered(user=new_user, session=self.session)
277 277 self.request.registry.notify(event)
278 278 self.session.flash(
279 279 _('You have successfully registered with RhodeCode'),
280 280 queue='success')
281 281 Session().commit()
282 282
283 283 redirect_ro = self.request.route_path('login')
284 284 raise HTTPFound(redirect_ro)
285 285
286 286 except formencode.Invalid as errors:
287 287 errors.value.pop('password', None)
288 288 errors.value.pop('password_confirmation', None)
289 289 return self.register(
290 290 defaults=errors.value, errors=errors.error_dict)
291 291
292 292 except UserCreationError as e:
293 293 # container auth or other auth functions that create users on
294 294 # the fly can throw this exception signaling that there's issue
295 295 # with user creation, explanation should be provided in
296 296 # Exception itself
297 297 self.session.flash(e, queue='error')
298 298 return self.register()
299 299
300 300 @view_config(
301 301 route_name='reset_password', request_method=('GET', 'POST'),
302 302 renderer='rhodecode:templates/password_reset.mako')
303 303 def password_reset(self):
304 304 captcha = self._get_captcha_data()
305 305
306 306 render_ctx = {
307 307 'captcha_active': captcha.active,
308 308 'captcha_public_key': captcha.public_key,
309 309 'defaults': {},
310 310 'errors': {},
311 311 }
312 312
313 313 # always send implicit message to prevent from discovery of
314 314 # matching emails
315 315 msg = _('If such email exists, a password reset link was sent to it.')
316 316
317 317 if self.request.POST:
318 318 if h.HasPermissionAny('hg.password_reset.disabled')():
319 319 _email = self.request.POST.get('email', '')
320 320 log.error('Failed attempt to reset password for `%s`.', _email)
321 321 self.session.flash(_('Password reset has been disabled.'),
322 322 queue='error')
323 323 return HTTPFound(self.request.route_path('reset_password'))
324 324
325 325 password_reset_form = PasswordResetForm()()
326 326 try:
327 327 form_result = password_reset_form.to_python(
328 328 self.request.params)
329 329 user_email = form_result['email']
330 330
331 331 if captcha.active:
332 332 response = submit(
333 333 self.request.params.get('recaptcha_challenge_field'),
334 334 self.request.params.get('recaptcha_response_field'),
335 335 private_key=captcha.private_key,
336 336 remoteip=get_ip_addr(self.request.environ))
337 337 if not response.is_valid:
338 338 _value = form_result
339 339 _msg = _('Bad captcha')
340 340 error_dict = {'recaptcha_field': _msg}
341 341 raise formencode.Invalid(
342 342 _msg, _value, None, error_dict=error_dict)
343 343
344 344 # Generate reset URL and send mail.
345 345 user = User.get_by_email(user_email)
346 346
347 347 # generate password reset token that expires in 10minutes
348 348 desc = 'Generated token for password reset from {}'.format(
349 349 datetime.datetime.now().isoformat())
350 350 reset_token = AuthTokenModel().create(
351 351 user, lifetime=10,
352 352 description=desc,
353 353 role=UserApiKeys.ROLE_PASSWORD_RESET)
354 354 Session().commit()
355 355
356 356 log.debug('Successfully created password recovery token')
357 357 password_reset_url = self.request.route_url(
358 358 'reset_password_confirmation',
359 359 _query={'key': reset_token.api_key})
360 360 UserModel().reset_password_link(
361 361 form_result, password_reset_url)
362 362 # Display success message and redirect.
363 363 self.session.flash(msg, queue='success')
364 364
365 365 action_data = {'email': user_email,
366 366 'user_agent': self.request.user_agent}
367 367 audit_logger.store_web(
368 368 'user.password.reset_request', action_data=action_data,
369 369 user=self._rhodecode_user, commit=True)
370 370 return HTTPFound(self.request.route_path('reset_password'))
371 371
372 372 except formencode.Invalid as errors:
373 373 render_ctx.update({
374 374 'defaults': errors.value,
375 375 'errors': errors.error_dict,
376 376 })
377 377 if not self.request.params.get('email'):
378 378 # case of empty email, we want to report that
379 379 return render_ctx
380 380
381 381 if 'recaptcha_field' in errors.error_dict:
382 382 # case of failed captcha
383 383 return render_ctx
384 384
385 385 log.debug('faking response on invalid password reset')
386 386 # make this take 2s, to prevent brute forcing.
387 387 time.sleep(2)
388 388 self.session.flash(msg, queue='success')
389 389 return HTTPFound(self.request.route_path('reset_password'))
390 390
391 391 return render_ctx
392 392
393 393 @view_config(route_name='reset_password_confirmation',
394 394 request_method='GET')
395 395 def password_reset_confirmation(self):
396 396
397 397 if self.request.GET and self.request.GET.get('key'):
398 398 # make this take 2s, to prevent brute forcing.
399 399 time.sleep(2)
400 400
401 401 token = AuthTokenModel().get_auth_token(
402 402 self.request.GET.get('key'))
403 403
404 404 # verify token is the correct role
405 405 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
406 406 log.debug('Got token with role:%s expected is %s',
407 407 getattr(token, 'role', 'EMPTY_TOKEN'),
408 408 UserApiKeys.ROLE_PASSWORD_RESET)
409 409 self.session.flash(
410 410 _('Given reset token is invalid'), queue='error')
411 411 return HTTPFound(self.request.route_path('reset_password'))
412 412
413 413 try:
414 414 owner = token.user
415 415 data = {'email': owner.email, 'token': token.api_key}
416 416 UserModel().reset_password(data)
417 417 self.session.flash(
418 418 _('Your password reset was successful, '
419 419 'a new password has been sent to your email'),
420 420 queue='success')
421 421 except Exception as e:
422 422 log.error(e)
423 423 return HTTPFound(self.request.route_path('reset_password'))
424 424
425 425 return HTTPFound(self.request.route_path('login'))
@@ -1,523 +1,523 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import re
22 22
23 23 import mock
24 24 import pytest
25 25
26 26 from rhodecode.apps.repository.views.repo_summary import RepoSummaryView
27 27 from rhodecode.lib import helpers as h
28 28 from rhodecode.lib.compat import OrderedDict
29 29 from rhodecode.lib.utils2 import AttributeDict, safe_str
30 30 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
31 31 from rhodecode.model.db import Repository
32 32 from rhodecode.model.meta import Session
33 33 from rhodecode.model.repo import RepoModel
34 34 from rhodecode.model.scm import ScmModel
35 35 from rhodecode.tests import assert_session_flash
36 36 from rhodecode.tests.fixture import Fixture
37 37 from rhodecode.tests.utils import AssertResponse, repo_on_filesystem
38 38
39 39
40 40 fixture = Fixture()
41 41
42 42
43 43 def route_path(name, params=None, **kwargs):
44 44 import urllib
45 45
46 46 base_url = {
47 47 'repo_summary': '/{repo_name}',
48 48 'repo_stats': '/{repo_name}/repo_stats/{commit_id}',
49 49 'repo_refs_data': '/{repo_name}/refs-data',
50 50 'repo_refs_changelog_data': '/{repo_name}/refs-data-changelog',
51 51 'repo_creating_check': '/{repo_name}/repo_creating_check',
52 52 }[name].format(**kwargs)
53 53
54 54 if params:
55 55 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
56 56 return base_url
57 57
58 58
59 59 @pytest.mark.usefixtures('app')
60 60 class TestSummaryView(object):
61 61 def test_index(self, autologin_user, backend, http_host_only_stub):
62 62 repo_id = backend.repo.repo_id
63 63 repo_name = backend.repo_name
64 64 with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy',
65 65 return_value=False):
66 66 response = self.app.get(
67 67 route_path('repo_summary', repo_name=repo_name))
68 68
69 69 # repo type
70 70 response.mustcontain(
71 71 '<i class="icon-%s">' % (backend.alias, )
72 72 )
73 73 # public/private
74 74 response.mustcontain(
75 75 """<i class="icon-unlock-alt">"""
76 76 )
77 77
78 78 # clone url...
79 79 response.mustcontain(
80 80 'id="clone_url" readonly="readonly"'
81 81 ' value="http://test_admin@%s/%s"' % (http_host_only_stub, repo_name, ))
82 82 response.mustcontain(
83 83 'id="clone_url_id" readonly="readonly"'
84 84 ' value="http://test_admin@%s/_%s"' % (http_host_only_stub, repo_id, ))
85 85
86 86 def test_index_svn_without_proxy(
87 87 self, autologin_user, backend_svn, http_host_only_stub):
88 88 repo_id = backend_svn.repo.repo_id
89 89 repo_name = backend_svn.repo_name
90 90 response = self.app.get(route_path('repo_summary', repo_name=repo_name))
91 91 # clone url...
92 92 response.mustcontain(
93 93 'id="clone_url" disabled'
94 94 ' value="http://test_admin@%s/%s"' % (http_host_only_stub, repo_name, ))
95 95 response.mustcontain(
96 96 'id="clone_url_id" disabled'
97 97 ' value="http://test_admin@%s/_%s"' % (http_host_only_stub, repo_id, ))
98 98
99 99 def test_index_with_trailing_slash(
100 100 self, autologin_user, backend, http_host_only_stub):
101 101
102 102 repo_id = backend.repo.repo_id
103 103 repo_name = backend.repo_name
104 104 with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy',
105 105 return_value=False):
106 106 response = self.app.get(
107 107 route_path('repo_summary', repo_name=repo_name) + '/',
108 108 status=200)
109 109
110 110 # clone url...
111 111 response.mustcontain(
112 112 'id="clone_url" readonly="readonly"'
113 113 ' value="http://test_admin@%s/%s"' % (http_host_only_stub, repo_name, ))
114 114 response.mustcontain(
115 115 'id="clone_url_id" readonly="readonly"'
116 116 ' value="http://test_admin@%s/_%s"' % (http_host_only_stub, repo_id, ))
117 117
118 118 def test_index_by_id(self, autologin_user, backend):
119 119 repo_id = backend.repo.repo_id
120 120 response = self.app.get(
121 121 route_path('repo_summary', repo_name='_%s' % (repo_id,)))
122 122
123 123 # repo type
124 124 response.mustcontain(
125 125 '<i class="icon-%s">' % (backend.alias, )
126 126 )
127 127 # public/private
128 128 response.mustcontain(
129 129 """<i class="icon-unlock-alt">"""
130 130 )
131 131
132 132 def test_index_by_repo_having_id_path_in_name_hg(self, autologin_user):
133 133 fixture.create_repo(name='repo_1')
134 134 response = self.app.get(route_path('repo_summary', repo_name='repo_1'))
135 135
136 136 try:
137 137 response.mustcontain("repo_1")
138 138 finally:
139 139 RepoModel().delete(Repository.get_by_repo_name('repo_1'))
140 140 Session().commit()
141 141
142 142 def test_index_with_anonymous_access_disabled(
143 143 self, backend, disable_anonymous_user):
144 144 response = self.app.get(
145 145 route_path('repo_summary', repo_name=backend.repo_name), status=302)
146 146 assert 'login' in response.location
147 147
148 148 def _enable_stats(self, repo):
149 149 r = Repository.get_by_repo_name(repo)
150 150 r.enable_statistics = True
151 151 Session().add(r)
152 152 Session().commit()
153 153
154 154 expected_trending = {
155 155 'hg': {
156 156 "py": {"count": 68, "desc": ["Python"]},
157 157 "rst": {"count": 16, "desc": ["Rst"]},
158 158 "css": {"count": 2, "desc": ["Css"]},
159 159 "sh": {"count": 2, "desc": ["Bash"]},
160 160 "bat": {"count": 1, "desc": ["Batch"]},
161 161 "cfg": {"count": 1, "desc": ["Ini"]},
162 162 "html": {"count": 1, "desc": ["EvoqueHtml", "Html"]},
163 163 "ini": {"count": 1, "desc": ["Ini"]},
164 164 "js": {"count": 1, "desc": ["Javascript"]},
165 165 "makefile": {"count": 1, "desc": ["Makefile", "Makefile"]}
166 166 },
167 167 'git': {
168 168 "py": {"count": 68, "desc": ["Python"]},
169 169 "rst": {"count": 16, "desc": ["Rst"]},
170 170 "css": {"count": 2, "desc": ["Css"]},
171 171 "sh": {"count": 2, "desc": ["Bash"]},
172 172 "bat": {"count": 1, "desc": ["Batch"]},
173 173 "cfg": {"count": 1, "desc": ["Ini"]},
174 174 "html": {"count": 1, "desc": ["EvoqueHtml", "Html"]},
175 175 "ini": {"count": 1, "desc": ["Ini"]},
176 176 "js": {"count": 1, "desc": ["Javascript"]},
177 177 "makefile": {"count": 1, "desc": ["Makefile", "Makefile"]}
178 178 },
179 179 'svn': {
180 180 "py": {"count": 75, "desc": ["Python"]},
181 181 "rst": {"count": 16, "desc": ["Rst"]},
182 182 "html": {"count": 11, "desc": ["EvoqueHtml", "Html"]},
183 183 "css": {"count": 2, "desc": ["Css"]},
184 184 "bat": {"count": 1, "desc": ["Batch"]},
185 185 "cfg": {"count": 1, "desc": ["Ini"]},
186 186 "ini": {"count": 1, "desc": ["Ini"]},
187 187 "js": {"count": 1, "desc": ["Javascript"]},
188 188 "makefile": {"count": 1, "desc": ["Makefile", "Makefile"]},
189 189 "sh": {"count": 1, "desc": ["Bash"]}
190 190 },
191 191 }
192 192
193 193 def test_repo_stats(self, autologin_user, backend, xhr_header):
194 194 response = self.app.get(
195 195 route_path(
196 196 'repo_stats', repo_name=backend.repo_name, commit_id='tip'),
197 197 extra_environ=xhr_header,
198 198 status=200)
199 199 assert re.match(r'6[\d\.]+ KiB', response.json['size'])
200 200
201 201 def test_repo_stats_code_stats_enabled(self, autologin_user, backend, xhr_header):
202 202 repo_name = backend.repo_name
203 203
204 204 # codes stats
205 205 self._enable_stats(repo_name)
206 206 ScmModel().mark_for_invalidation(repo_name)
207 207
208 208 response = self.app.get(
209 209 route_path(
210 210 'repo_stats', repo_name=backend.repo_name, commit_id='tip'),
211 211 extra_environ=xhr_header,
212 212 status=200)
213 213
214 214 expected_data = self.expected_trending[backend.alias]
215 215 returned_stats = response.json['code_stats']
216 216 for k, v in expected_data.items():
217 217 assert v == returned_stats[k]
218 218
219 219 def test_repo_refs_data(self, backend):
220 220 response = self.app.get(
221 221 route_path('repo_refs_data', repo_name=backend.repo_name),
222 222 status=200)
223 223
224 224 # Ensure that there is the correct amount of items in the result
225 225 repo = backend.repo.scm_instance()
226 226 data = response.json['results']
227 227 items = sum(len(section['children']) for section in data)
228 228 repo_refs = len(repo.branches) + len(repo.tags) + len(repo.bookmarks)
229 229 assert items == repo_refs
230 230
231 231 def test_index_shows_missing_requirements_message(
232 232 self, backend, autologin_user):
233 233 repo_name = backend.repo_name
234 234 scm_patcher = mock.patch.object(
235 235 Repository, 'scm_instance', side_effect=RepositoryRequirementError)
236 236
237 237 with scm_patcher:
238 238 response = self.app.get(route_path('repo_summary', repo_name=repo_name))
239 239 assert_response = AssertResponse(response)
240 240 assert_response.element_contains(
241 241 '.main .alert-warning strong', 'Missing requirements')
242 242 assert_response.element_contains(
243 243 '.main .alert-warning',
244 244 'Commits cannot be displayed, because this repository '
245 245 'uses one or more extensions, which was not enabled.')
246 246
247 247 def test_missing_requirements_page_does_not_contains_switch_to(
248 248 self, autologin_user, backend):
249 249 repo_name = backend.repo_name
250 250 scm_patcher = mock.patch.object(
251 251 Repository, 'scm_instance', side_effect=RepositoryRequirementError)
252 252
253 253 with scm_patcher:
254 254 response = self.app.get(route_path('repo_summary', repo_name=repo_name))
255 255 response.mustcontain(no='Switch To')
256 256
257 257
258 258 @pytest.mark.usefixtures('app')
259 259 class TestRepoLocation(object):
260 260
261 261 @pytest.mark.parametrize("suffix", [u'', u'Δ…Δ™Ε‚'], ids=['', 'non-ascii'])
262 262 def test_missing_filesystem_repo(
263 263 self, autologin_user, backend, suffix, csrf_token):
264 264 repo = backend.create_repo(name_suffix=suffix)
265 265 repo_name = repo.repo_name
266 266
267 267 # delete from file system
268 268 RepoModel()._delete_filesystem_repo(repo)
269 269
270 270 # test if the repo is still in the database
271 271 new_repo = RepoModel().get_by_repo_name(repo_name)
272 272 assert new_repo.repo_name == repo_name
273 273
274 274 # check if repo is not in the filesystem
275 275 assert not repo_on_filesystem(repo_name)
276 276
277 277 response = self.app.get(
278 278 route_path('repo_summary', repo_name=safe_str(repo_name)), status=302)
279 279
280 280 msg = 'The repository `%s` cannot be loaded in filesystem. ' \
281 281 'Please check if it exist, or is not damaged.' % repo_name
282 282 assert_session_flash(response, msg)
283 283
284 284 @pytest.mark.parametrize("suffix", [u'', u'Δ…Δ™Ε‚'], ids=['', 'non-ascii'])
285 285 def test_missing_filesystem_repo_on_repo_check(
286 286 self, autologin_user, backend, suffix, csrf_token):
287 287 repo = backend.create_repo(name_suffix=suffix)
288 288 repo_name = repo.repo_name
289 289
290 290 # delete from file system
291 291 RepoModel()._delete_filesystem_repo(repo)
292 292
293 293 # test if the repo is still in the database
294 294 new_repo = RepoModel().get_by_repo_name(repo_name)
295 295 assert new_repo.repo_name == repo_name
296 296
297 297 # check if repo is not in the filesystem
298 298 assert not repo_on_filesystem(repo_name)
299 299
300 300 # flush the session
301 301 self.app.get(
302 302 route_path('repo_summary', repo_name=safe_str(repo_name)),
303 303 status=302)
304 304
305 305 response = self.app.get(
306 306 route_path('repo_creating_check', repo_name=safe_str(repo_name)),
307 307 status=200)
308 308 msg = 'The repository `%s` cannot be loaded in filesystem. ' \
309 309 'Please check if it exist, or is not damaged.' % repo_name
310 310 assert_session_flash(response, msg )
311 311
312 312
313 313 @pytest.fixture()
314 314 def summary_view(context_stub, request_stub, user_util):
315 315 """
316 316 Bootstrap view to test the view functions
317 317 """
318 318 request_stub.matched_route = AttributeDict(name='test_view')
319 319
320 request_stub.user = user_util.create_user().AuthUser
320 request_stub.user = user_util.create_user().AuthUser()
321 321 request_stub.db_repo = user_util.create_repo()
322 322
323 323 view = RepoSummaryView(context=context_stub, request=request_stub)
324 324 return view
325 325
326 326
327 327 @pytest.mark.usefixtures('app')
328 328 class TestCreateReferenceData(object):
329 329
330 330 @pytest.fixture
331 331 def example_refs(self):
332 332 section_1_refs = OrderedDict((('a', 'a_id'), ('b', 'b_id')))
333 333 example_refs = [
334 334 ('section_1', section_1_refs, 't1'),
335 335 ('section_2', {'c': 'c_id'}, 't2'),
336 336 ]
337 337 return example_refs
338 338
339 339 def test_generates_refs_based_on_commit_ids(self, example_refs, summary_view):
340 340 repo = mock.Mock()
341 341 repo.name = 'test-repo'
342 342 repo.alias = 'git'
343 343 full_repo_name = 'pytest-repo-group/' + repo.name
344 344
345 345 result = summary_view._create_reference_data(
346 346 repo, full_repo_name, example_refs)
347 347
348 348 expected_files_url = '/{}/files/'.format(full_repo_name)
349 349 expected_result = [
350 350 {
351 351 'children': [
352 352 {
353 353 'id': 'a', 'raw_id': 'a_id', 'text': 'a', 'type': 't1',
354 354 'files_url': expected_files_url + 'a/?at=a',
355 355 },
356 356 {
357 357 'id': 'b', 'raw_id': 'b_id', 'text': 'b', 'type': 't1',
358 358 'files_url': expected_files_url + 'b/?at=b',
359 359 }
360 360 ],
361 361 'text': 'section_1'
362 362 },
363 363 {
364 364 'children': [
365 365 {
366 366 'id': 'c', 'raw_id': 'c_id', 'text': 'c', 'type': 't2',
367 367 'files_url': expected_files_url + 'c/?at=c',
368 368 }
369 369 ],
370 370 'text': 'section_2'
371 371 }]
372 372 assert result == expected_result
373 373
374 374 def test_generates_refs_with_path_for_svn(self, example_refs, summary_view):
375 375 repo = mock.Mock()
376 376 repo.name = 'test-repo'
377 377 repo.alias = 'svn'
378 378 full_repo_name = 'pytest-repo-group/' + repo.name
379 379
380 380 result = summary_view._create_reference_data(
381 381 repo, full_repo_name, example_refs)
382 382
383 383 expected_files_url = '/{}/files/'.format(full_repo_name)
384 384 expected_result = [
385 385 {
386 386 'children': [
387 387 {
388 388 'id': 'a@a_id', 'raw_id': 'a_id',
389 389 'text': 'a', 'type': 't1',
390 390 'files_url': expected_files_url + 'a_id/a?at=a',
391 391 },
392 392 {
393 393 'id': 'b@b_id', 'raw_id': 'b_id',
394 394 'text': 'b', 'type': 't1',
395 395 'files_url': expected_files_url + 'b_id/b?at=b',
396 396 }
397 397 ],
398 398 'text': 'section_1'
399 399 },
400 400 {
401 401 'children': [
402 402 {
403 403 'id': 'c@c_id', 'raw_id': 'c_id',
404 404 'text': 'c', 'type': 't2',
405 405 'files_url': expected_files_url + 'c_id/c?at=c',
406 406 }
407 407 ],
408 408 'text': 'section_2'
409 409 }
410 410 ]
411 411 assert result == expected_result
412 412
413 413
414 414 class TestCreateFilesUrl(object):
415 415
416 416 def test_creates_non_svn_url(self, app, summary_view):
417 417 repo = mock.Mock()
418 418 repo.name = 'abcde'
419 419 full_repo_name = 'test-repo-group/' + repo.name
420 420 ref_name = 'branch1'
421 421 raw_id = 'deadbeef0123456789'
422 422 is_svn = False
423 423
424 424 with mock.patch('rhodecode.lib.helpers.route_path') as url_mock:
425 425 result = summary_view._create_files_url(
426 426 repo, full_repo_name, ref_name, raw_id, is_svn)
427 427 url_mock.assert_called_once_with(
428 428 'repo_files', repo_name=full_repo_name, commit_id=ref_name,
429 429 f_path='', _query=dict(at=ref_name))
430 430 assert result == url_mock.return_value
431 431
432 432 def test_creates_svn_url(self, app, summary_view):
433 433 repo = mock.Mock()
434 434 repo.name = 'abcde'
435 435 full_repo_name = 'test-repo-group/' + repo.name
436 436 ref_name = 'branch1'
437 437 raw_id = 'deadbeef0123456789'
438 438 is_svn = True
439 439
440 440 with mock.patch('rhodecode.lib.helpers.route_path') as url_mock:
441 441 result = summary_view._create_files_url(
442 442 repo, full_repo_name, ref_name, raw_id, is_svn)
443 443 url_mock.assert_called_once_with(
444 444 'repo_files', repo_name=full_repo_name, f_path=ref_name,
445 445 commit_id=raw_id, _query=dict(at=ref_name))
446 446 assert result == url_mock.return_value
447 447
448 448 def test_name_has_slashes(self, app, summary_view):
449 449 repo = mock.Mock()
450 450 repo.name = 'abcde'
451 451 full_repo_name = 'test-repo-group/' + repo.name
452 452 ref_name = 'branch1/branch2'
453 453 raw_id = 'deadbeef0123456789'
454 454 is_svn = False
455 455
456 456 with mock.patch('rhodecode.lib.helpers.route_path') as url_mock:
457 457 result = summary_view._create_files_url(
458 458 repo, full_repo_name, ref_name, raw_id, is_svn)
459 459 url_mock.assert_called_once_with(
460 460 'repo_files', repo_name=full_repo_name, commit_id=raw_id,
461 461 f_path='', _query=dict(at=ref_name))
462 462 assert result == url_mock.return_value
463 463
464 464
465 465 class TestReferenceItems(object):
466 466 repo = mock.Mock()
467 467 repo.name = 'pytest-repo'
468 468 repo_full_name = 'pytest-repo-group/' + repo.name
469 469 ref_type = 'branch'
470 470 fake_url = '/abcde/'
471 471
472 472 @staticmethod
473 473 def _format_function(name, id_):
474 474 return 'format_function_{}_{}'.format(name, id_)
475 475
476 476 def test_creates_required_amount_of_items(self, summary_view):
477 477 amount = 100
478 478 refs = {
479 479 'ref{}'.format(i): '{0:040d}'.format(i)
480 480 for i in range(amount)
481 481 }
482 482
483 483 url_patcher = mock.patch.object(summary_view, '_create_files_url')
484 484 svn_patcher = mock.patch('rhodecode.lib.helpers.is_svn',
485 485 return_value=False)
486 486
487 487 with url_patcher as url_mock, svn_patcher:
488 488 result = summary_view._create_reference_items(
489 489 self.repo, self.repo_full_name, refs, self.ref_type,
490 490 self._format_function)
491 491 assert len(result) == amount
492 492 assert url_mock.call_count == amount
493 493
494 494 def test_single_item_details(self, summary_view):
495 495 ref_name = 'ref1'
496 496 ref_id = 'deadbeef'
497 497 refs = {
498 498 ref_name: ref_id
499 499 }
500 500
501 501 svn_patcher = mock.patch('rhodecode.lib.helpers.is_svn',
502 502 return_value=False)
503 503
504 504 url_patcher = mock.patch.object(
505 505 summary_view, '_create_files_url', return_value=self.fake_url)
506 506
507 507 with url_patcher as url_mock, svn_patcher:
508 508 result = summary_view._create_reference_items(
509 509 self.repo, self.repo_full_name, refs, self.ref_type,
510 510 self._format_function)
511 511
512 512 url_mock.assert_called_once_with(
513 513 self.repo, self.repo_full_name, ref_name, ref_id, False)
514 514 expected_result = [
515 515 {
516 516 'text': ref_name,
517 517 'id': self._format_function(ref_name, ref_id),
518 518 'raw_id': ref_id,
519 519 'type': self.ref_type,
520 520 'files_url': self.fake_url
521 521 }
522 522 ]
523 523 assert result == expected_result
@@ -1,284 +1,284 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 RhodeCode authentication plugin for Atlassian CROWD
23 23 """
24 24
25 25
26 26 import colander
27 27 import base64
28 28 import logging
29 29 import urllib2
30 30
31 31 from rhodecode.translation import _
32 32 from rhodecode.authentication.base import (
33 33 RhodeCodeExternalAuthPlugin, hybrid_property)
34 34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
35 35 from rhodecode.authentication.routes import AuthnPluginResourceBase
36 36 from rhodecode.lib.colander_utils import strip_whitespace
37 37 from rhodecode.lib.ext_json import json, formatted_json
38 38 from rhodecode.model.db import User
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 def plugin_factory(plugin_id, *args, **kwds):
44 44 """
45 45 Factory function that is called during plugin discovery.
46 46 It returns the plugin instance.
47 47 """
48 48 plugin = RhodeCodeAuthPlugin(plugin_id)
49 49 return plugin
50 50
51 51
52 52 class CrowdAuthnResource(AuthnPluginResourceBase):
53 53 pass
54 54
55 55
56 56 class CrowdSettingsSchema(AuthnPluginSettingsSchemaBase):
57 57 host = colander.SchemaNode(
58 58 colander.String(),
59 59 default='127.0.0.1',
60 60 description=_('The FQDN or IP of the Atlassian CROWD Server'),
61 61 preparer=strip_whitespace,
62 62 title=_('Host'),
63 63 widget='string')
64 64 port = colander.SchemaNode(
65 65 colander.Int(),
66 66 default=8095,
67 67 description=_('The Port in use by the Atlassian CROWD Server'),
68 68 preparer=strip_whitespace,
69 69 title=_('Port'),
70 70 validator=colander.Range(min=0, max=65536),
71 71 widget='int')
72 72 app_name = colander.SchemaNode(
73 73 colander.String(),
74 74 default='',
75 75 description=_('The Application Name to authenticate to CROWD'),
76 76 preparer=strip_whitespace,
77 77 title=_('Application Name'),
78 78 widget='string')
79 79 app_password = colander.SchemaNode(
80 80 colander.String(),
81 81 default='',
82 82 description=_('The password to authenticate to CROWD'),
83 83 preparer=strip_whitespace,
84 84 title=_('Application Password'),
85 85 widget='password')
86 86 admin_groups = colander.SchemaNode(
87 87 colander.String(),
88 88 default='',
89 89 description=_('A comma separated list of group names that identify '
90 90 'users as RhodeCode Administrators'),
91 91 missing='',
92 92 preparer=strip_whitespace,
93 93 title=_('Admin Groups'),
94 94 widget='string')
95 95
96 96
97 97 class CrowdServer(object):
98 98 def __init__(self, *args, **kwargs):
99 99 """
100 100 Create a new CrowdServer object that points to IP/Address 'host',
101 101 on the given port, and using the given method (https/http). user and
102 102 passwd can be set here or with set_credentials. If unspecified,
103 103 "version" defaults to "latest".
104 104
105 105 example::
106 106
107 107 cserver = CrowdServer(host="127.0.0.1",
108 108 port="8095",
109 109 user="some_app",
110 110 passwd="some_passwd",
111 111 version="1")
112 112 """
113 113 if not "port" in kwargs:
114 114 kwargs["port"] = "8095"
115 115 self._logger = kwargs.get("logger", logging.getLogger(__name__))
116 116 self._uri = "%s://%s:%s/crowd" % (kwargs.get("method", "http"),
117 117 kwargs.get("host", "127.0.0.1"),
118 118 kwargs.get("port", "8095"))
119 119 self.set_credentials(kwargs.get("user", ""),
120 120 kwargs.get("passwd", ""))
121 121 self._version = kwargs.get("version", "latest")
122 122 self._url_list = None
123 123 self._appname = "crowd"
124 124
125 125 def set_credentials(self, user, passwd):
126 126 self.user = user
127 127 self.passwd = passwd
128 128 self._make_opener()
129 129
130 130 def _make_opener(self):
131 131 mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
132 132 mgr.add_password(None, self._uri, self.user, self.passwd)
133 133 handler = urllib2.HTTPBasicAuthHandler(mgr)
134 134 self.opener = urllib2.build_opener(handler)
135 135
136 136 def _request(self, url, body=None, headers=None,
137 137 method=None, noformat=False,
138 138 empty_response_ok=False):
139 139 _headers = {"Content-type": "application/json",
140 140 "Accept": "application/json"}
141 141 if self.user and self.passwd:
142 142 authstring = base64.b64encode("%s:%s" % (self.user, self.passwd))
143 143 _headers["Authorization"] = "Basic %s" % authstring
144 144 if headers:
145 145 _headers.update(headers)
146 146 log.debug("Sent crowd: \n%s"
147 147 % (formatted_json({"url": url, "body": body,
148 148 "headers": _headers})))
149 149 request = urllib2.Request(url, body, _headers)
150 150 if method:
151 151 request.get_method = lambda: method
152 152
153 153 global msg
154 154 msg = ""
155 155 try:
156 156 rdoc = self.opener.open(request)
157 157 msg = "".join(rdoc.readlines())
158 158 if not msg and empty_response_ok:
159 159 rval = {}
160 160 rval["status"] = True
161 161 rval["error"] = "Response body was empty"
162 162 elif not noformat:
163 163 rval = json.loads(msg)
164 164 rval["status"] = True
165 165 else:
166 166 rval = "".join(rdoc.readlines())
167 167 except Exception as e:
168 168 if not noformat:
169 169 rval = {"status": False,
170 170 "body": body,
171 171 "error": str(e) + "\n" + msg}
172 172 else:
173 173 rval = None
174 174 return rval
175 175
176 176 def user_auth(self, username, password):
177 177 """Authenticate a user against crowd. Returns brief information about
178 178 the user."""
179 179 url = ("%s/rest/usermanagement/%s/authentication?username=%s"
180 180 % (self._uri, self._version, username))
181 181 body = json.dumps({"value": password})
182 182 return self._request(url, body)
183 183
184 184 def user_groups(self, username):
185 185 """Retrieve a list of groups to which this user belongs."""
186 186 url = ("%s/rest/usermanagement/%s/user/group/nested?username=%s"
187 187 % (self._uri, self._version, username))
188 188 return self._request(url)
189 189
190 190
191 191 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
192 192 _settings_unsafe_keys = ['app_password']
193 193
194 194 def includeme(self, config):
195 195 config.add_authn_plugin(self)
196 196 config.add_authn_resource(self.get_id(), CrowdAuthnResource(self))
197 197 config.add_view(
198 198 'rhodecode.authentication.views.AuthnPluginViewBase',
199 199 attr='settings_get',
200 200 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
201 201 request_method='GET',
202 202 route_name='auth_home',
203 203 context=CrowdAuthnResource)
204 204 config.add_view(
205 205 'rhodecode.authentication.views.AuthnPluginViewBase',
206 206 attr='settings_post',
207 207 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
208 208 request_method='POST',
209 209 route_name='auth_home',
210 210 context=CrowdAuthnResource)
211 211
212 212 def get_settings_schema(self):
213 213 return CrowdSettingsSchema()
214 214
215 215 def get_display_name(self):
216 216 return _('CROWD')
217 217
218 218 @hybrid_property
219 219 def name(self):
220 220 return "crowd"
221 221
222 222 def use_fake_password(self):
223 223 return True
224 224
225 225 def user_activation_state(self):
226 def_user_perms = User.get_default_user().AuthUser.permissions['global']
226 def_user_perms = User.get_default_user().AuthUser().permissions['global']
227 227 return 'hg.extern_activate.auto' in def_user_perms
228 228
229 229 def auth(self, userobj, username, password, settings, **kwargs):
230 230 """
231 231 Given a user object (which may be null), username, a plaintext password,
232 232 and a settings object (containing all the keys needed as listed in settings()),
233 233 authenticate this user's login attempt.
234 234
235 235 Return None on failure. On success, return a dictionary of the form:
236 236
237 237 see: RhodeCodeAuthPluginBase.auth_func_attrs
238 238 This is later validated for correctness
239 239 """
240 240 if not username or not password:
241 241 log.debug('Empty username or password skipping...')
242 242 return None
243 243
244 244 log.debug("Crowd settings: \n%s" % (formatted_json(settings)))
245 245 server = CrowdServer(**settings)
246 246 server.set_credentials(settings["app_name"], settings["app_password"])
247 247 crowd_user = server.user_auth(username, password)
248 248 log.debug("Crowd returned: \n%s" % (formatted_json(crowd_user)))
249 249 if not crowd_user["status"]:
250 250 return None
251 251
252 252 res = server.user_groups(crowd_user["name"])
253 253 log.debug("Crowd groups: \n%s" % (formatted_json(res)))
254 254 crowd_user["groups"] = [x["name"] for x in res["groups"]]
255 255
256 256 # old attrs fetched from RhodeCode database
257 257 admin = getattr(userobj, 'admin', False)
258 258 active = getattr(userobj, 'active', True)
259 259 email = getattr(userobj, 'email', '')
260 260 username = getattr(userobj, 'username', username)
261 261 firstname = getattr(userobj, 'firstname', '')
262 262 lastname = getattr(userobj, 'lastname', '')
263 263 extern_type = getattr(userobj, 'extern_type', '')
264 264
265 265 user_attrs = {
266 266 'username': username,
267 267 'firstname': crowd_user["first-name"] or firstname,
268 268 'lastname': crowd_user["last-name"] or lastname,
269 269 'groups': crowd_user["groups"],
270 270 'email': crowd_user["email"] or email,
271 271 'admin': admin,
272 272 'active': active,
273 273 'active_from_extern': crowd_user.get('active'),
274 274 'extern_name': crowd_user["name"],
275 275 'extern_type': extern_type,
276 276 }
277 277
278 278 # set an admin if we're in admin_groups of crowd
279 279 for group in settings["admin_groups"]:
280 280 if group in user_attrs["groups"]:
281 281 user_attrs["admin"] = True
282 282 log.debug("Final crowd user object: \n%s" % (formatted_json(user_attrs)))
283 283 log.info('user %s authenticated correctly' % user_attrs['username'])
284 284 return user_attrs
@@ -1,224 +1,224 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import colander
22 22 import logging
23 23
24 24 from rhodecode.translation import _
25 25 from rhodecode.authentication.base import (
26 26 RhodeCodeExternalAuthPlugin, hybrid_property)
27 27 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
28 28 from rhodecode.authentication.routes import AuthnPluginResourceBase
29 29 from rhodecode.lib.colander_utils import strip_whitespace
30 30 from rhodecode.lib.utils2 import str2bool, safe_unicode
31 31 from rhodecode.model.db import User
32 32
33 33
34 34 log = logging.getLogger(__name__)
35 35
36 36
37 37 def plugin_factory(plugin_id, *args, **kwds):
38 38 """
39 39 Factory function that is called during plugin discovery.
40 40 It returns the plugin instance.
41 41 """
42 42 plugin = RhodeCodeAuthPlugin(plugin_id)
43 43 return plugin
44 44
45 45
46 46 class HeadersAuthnResource(AuthnPluginResourceBase):
47 47 pass
48 48
49 49
50 50 class HeadersSettingsSchema(AuthnPluginSettingsSchemaBase):
51 51 header = colander.SchemaNode(
52 52 colander.String(),
53 53 default='REMOTE_USER',
54 54 description=_('Header to extract the user from'),
55 55 preparer=strip_whitespace,
56 56 title=_('Header'),
57 57 widget='string')
58 58 fallback_header = colander.SchemaNode(
59 59 colander.String(),
60 60 default='HTTP_X_FORWARDED_USER',
61 61 description=_('Header to extract the user from when main one fails'),
62 62 preparer=strip_whitespace,
63 63 title=_('Fallback header'),
64 64 widget='string')
65 65 clean_username = colander.SchemaNode(
66 66 colander.Boolean(),
67 67 default=True,
68 68 description=_('Perform cleaning of user, if passed user has @ in '
69 69 'username then first part before @ is taken. '
70 70 'If there\'s \\ in the username only the part after '
71 71 ' \\ is taken'),
72 72 missing=False,
73 73 title=_('Clean username'),
74 74 widget='bool')
75 75
76 76
77 77 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
78 78
79 79 def includeme(self, config):
80 80 config.add_authn_plugin(self)
81 81 config.add_authn_resource(self.get_id(), HeadersAuthnResource(self))
82 82 config.add_view(
83 83 'rhodecode.authentication.views.AuthnPluginViewBase',
84 84 attr='settings_get',
85 85 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
86 86 request_method='GET',
87 87 route_name='auth_home',
88 88 context=HeadersAuthnResource)
89 89 config.add_view(
90 90 'rhodecode.authentication.views.AuthnPluginViewBase',
91 91 attr='settings_post',
92 92 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
93 93 request_method='POST',
94 94 route_name='auth_home',
95 95 context=HeadersAuthnResource)
96 96
97 97 def get_display_name(self):
98 98 return _('Headers')
99 99
100 100 def get_settings_schema(self):
101 101 return HeadersSettingsSchema()
102 102
103 103 @hybrid_property
104 104 def name(self):
105 105 return 'headers'
106 106
107 107 @property
108 108 def is_headers_auth(self):
109 109 return True
110 110
111 111 def use_fake_password(self):
112 112 return True
113 113
114 114 def user_activation_state(self):
115 def_user_perms = User.get_default_user().AuthUser.permissions['global']
115 def_user_perms = User.get_default_user().AuthUser().permissions['global']
116 116 return 'hg.extern_activate.auto' in def_user_perms
117 117
118 118 def _clean_username(self, username):
119 119 # Removing realm and domain from username
120 120 username = username.split('@')[0]
121 121 username = username.rsplit('\\')[-1]
122 122 return username
123 123
124 124 def _get_username(self, environ, settings):
125 125 username = None
126 126 environ = environ or {}
127 127 if not environ:
128 128 log.debug('got empty environ: %s' % environ)
129 129
130 130 settings = settings or {}
131 131 if settings.get('header'):
132 132 header = settings.get('header')
133 133 username = environ.get(header)
134 134 log.debug('extracted %s:%s' % (header, username))
135 135
136 136 # fallback mode
137 137 if not username and settings.get('fallback_header'):
138 138 header = settings.get('fallback_header')
139 139 username = environ.get(header)
140 140 log.debug('extracted %s:%s' % (header, username))
141 141
142 142 if username and str2bool(settings.get('clean_username')):
143 143 log.debug('Received username `%s` from headers' % username)
144 144 username = self._clean_username(username)
145 145 log.debug('New cleanup user is:%s' % username)
146 146 return username
147 147
148 148 def get_user(self, username=None, **kwargs):
149 149 """
150 150 Helper method for user fetching in plugins, by default it's using
151 151 simple fetch by username, but this method can be custimized in plugins
152 152 eg. headers auth plugin to fetch user by environ params
153 153 :param username: username if given to fetch
154 154 :param kwargs: extra arguments needed for user fetching.
155 155 """
156 156 environ = kwargs.get('environ') or {}
157 157 settings = kwargs.get('settings') or {}
158 158 username = self._get_username(environ, settings)
159 159 # we got the username, so use default method now
160 160 return super(RhodeCodeAuthPlugin, self).get_user(username)
161 161
162 162 def auth(self, userobj, username, password, settings, **kwargs):
163 163 """
164 164 Get's the headers_auth username (or email). It tries to get username
165 165 from REMOTE_USER if this plugin is enabled, if that fails
166 166 it tries to get username from HTTP_X_FORWARDED_USER if fallback header
167 167 is set. clean_username extracts the username from this data if it's
168 168 having @ in it.
169 169 Return None on failure. On success, return a dictionary of the form:
170 170
171 171 see: RhodeCodeAuthPluginBase.auth_func_attrs
172 172
173 173 :param userobj:
174 174 :param username:
175 175 :param password:
176 176 :param settings:
177 177 :param kwargs:
178 178 """
179 179 environ = kwargs.get('environ')
180 180 if not environ:
181 181 log.debug('Empty environ data skipping...')
182 182 return None
183 183
184 184 if not userobj:
185 185 userobj = self.get_user('', environ=environ, settings=settings)
186 186
187 187 # we don't care passed username/password for headers auth plugins.
188 188 # only way to log in is using environ
189 189 username = None
190 190 if userobj:
191 191 username = getattr(userobj, 'username')
192 192
193 193 if not username:
194 194 # we don't have any objects in DB user doesn't exist extract
195 195 # username from environ based on the settings
196 196 username = self._get_username(environ, settings)
197 197
198 198 # if cannot fetch username, it's a no-go for this plugin to proceed
199 199 if not username:
200 200 return None
201 201
202 202 # old attrs fetched from RhodeCode database
203 203 admin = getattr(userobj, 'admin', False)
204 204 active = getattr(userobj, 'active', True)
205 205 email = getattr(userobj, 'email', '')
206 206 firstname = getattr(userobj, 'firstname', '')
207 207 lastname = getattr(userobj, 'lastname', '')
208 208 extern_type = getattr(userobj, 'extern_type', '')
209 209
210 210 user_attrs = {
211 211 'username': username,
212 212 'firstname': safe_unicode(firstname or username),
213 213 'lastname': safe_unicode(lastname or ''),
214 214 'groups': [],
215 215 'email': email or '',
216 216 'admin': admin or False,
217 217 'active': active,
218 218 'active_from_extern': True,
219 219 'extern_name': username,
220 220 'extern_type': extern_type,
221 221 }
222 222
223 223 log.info('user `%s` authenticated correctly' % user_attrs['username'])
224 224 return user_attrs
@@ -1,166 +1,166 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 RhodeCode authentication plugin for Jasig CAS
23 23 http://www.jasig.org/cas
24 24 """
25 25
26 26
27 27 import colander
28 28 import logging
29 29 import rhodecode
30 30 import urllib
31 31 import urllib2
32 32
33 33 from rhodecode.translation import _
34 34 from rhodecode.authentication.base import (
35 35 RhodeCodeExternalAuthPlugin, hybrid_property)
36 36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 37 from rhodecode.authentication.routes import AuthnPluginResourceBase
38 38 from rhodecode.lib.colander_utils import strip_whitespace
39 39 from rhodecode.lib.utils2 import safe_unicode
40 40 from rhodecode.model.db import User
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 def plugin_factory(plugin_id, *args, **kwds):
46 46 """
47 47 Factory function that is called during plugin discovery.
48 48 It returns the plugin instance.
49 49 """
50 50 plugin = RhodeCodeAuthPlugin(plugin_id)
51 51 return plugin
52 52
53 53
54 54 class JasigCasAuthnResource(AuthnPluginResourceBase):
55 55 pass
56 56
57 57
58 58 class JasigCasSettingsSchema(AuthnPluginSettingsSchemaBase):
59 59 service_url = colander.SchemaNode(
60 60 colander.String(),
61 61 default='https://domain.com/cas/v1/tickets',
62 62 description=_('The url of the Jasig CAS REST service'),
63 63 preparer=strip_whitespace,
64 64 title=_('URL'),
65 65 widget='string')
66 66
67 67
68 68 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
69 69
70 70 def includeme(self, config):
71 71 config.add_authn_plugin(self)
72 72 config.add_authn_resource(self.get_id(), JasigCasAuthnResource(self))
73 73 config.add_view(
74 74 'rhodecode.authentication.views.AuthnPluginViewBase',
75 75 attr='settings_get',
76 76 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
77 77 request_method='GET',
78 78 route_name='auth_home',
79 79 context=JasigCasAuthnResource)
80 80 config.add_view(
81 81 'rhodecode.authentication.views.AuthnPluginViewBase',
82 82 attr='settings_post',
83 83 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
84 84 request_method='POST',
85 85 route_name='auth_home',
86 86 context=JasigCasAuthnResource)
87 87
88 88 def get_settings_schema(self):
89 89 return JasigCasSettingsSchema()
90 90
91 91 def get_display_name(self):
92 92 return _('Jasig-CAS')
93 93
94 94 @hybrid_property
95 95 def name(self):
96 96 return "jasig-cas"
97 97
98 98 @property
99 99 def is_headers_auth(self):
100 100 return True
101 101
102 102 def use_fake_password(self):
103 103 return True
104 104
105 105 def user_activation_state(self):
106 def_user_perms = User.get_default_user().AuthUser.permissions['global']
106 def_user_perms = User.get_default_user().AuthUser().permissions['global']
107 107 return 'hg.extern_activate.auto' in def_user_perms
108 108
109 109 def auth(self, userobj, username, password, settings, **kwargs):
110 110 """
111 111 Given a user object (which may be null), username, a plaintext password,
112 112 and a settings object (containing all the keys needed as listed in settings()),
113 113 authenticate this user's login attempt.
114 114
115 115 Return None on failure. On success, return a dictionary of the form:
116 116
117 117 see: RhodeCodeAuthPluginBase.auth_func_attrs
118 118 This is later validated for correctness
119 119 """
120 120 if not username or not password:
121 121 log.debug('Empty username or password skipping...')
122 122 return None
123 123
124 124 log.debug("Jasig CAS settings: %s", settings)
125 125 params = urllib.urlencode({'username': username, 'password': password})
126 126 headers = {"Content-type": "application/x-www-form-urlencoded",
127 127 "Accept": "text/plain",
128 128 "User-Agent": "RhodeCode-auth-%s" % rhodecode.__version__}
129 129 url = settings["service_url"]
130 130
131 131 log.debug("Sent Jasig CAS: \n%s",
132 132 {"url": url, "body": params, "headers": headers})
133 133 request = urllib2.Request(url, params, headers)
134 134 try:
135 135 response = urllib2.urlopen(request)
136 136 except urllib2.HTTPError as e:
137 137 log.debug("HTTPError when requesting Jasig CAS (status code: %d)" % e.code)
138 138 return None
139 139 except urllib2.URLError as e:
140 140 log.debug("URLError when requesting Jasig CAS url: %s " % url)
141 141 return None
142 142
143 143 # old attrs fetched from RhodeCode database
144 144 admin = getattr(userobj, 'admin', False)
145 145 active = getattr(userobj, 'active', True)
146 146 email = getattr(userobj, 'email', '')
147 147 username = getattr(userobj, 'username', username)
148 148 firstname = getattr(userobj, 'firstname', '')
149 149 lastname = getattr(userobj, 'lastname', '')
150 150 extern_type = getattr(userobj, 'extern_type', '')
151 151
152 152 user_attrs = {
153 153 'username': username,
154 154 'firstname': safe_unicode(firstname or username),
155 155 'lastname': safe_unicode(lastname or ''),
156 156 'groups': [],
157 157 'email': email or '',
158 158 'admin': admin or False,
159 159 'active': active,
160 160 'active_from_extern': True,
161 161 'extern_name': username,
162 162 'extern_type': extern_type,
163 163 }
164 164
165 165 log.info('user %s authenticated correctly' % user_attrs['username'])
166 166 return user_attrs
@@ -1,480 +1,480 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 RhodeCode authentication plugin for LDAP
23 23 """
24 24
25 25
26 26 import colander
27 27 import logging
28 28 import traceback
29 29
30 30 from rhodecode.translation import _
31 31 from rhodecode.authentication.base import (
32 32 RhodeCodeExternalAuthPlugin, chop_at, hybrid_property)
33 33 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
34 34 from rhodecode.authentication.routes import AuthnPluginResourceBase
35 35 from rhodecode.lib.colander_utils import strip_whitespace
36 36 from rhodecode.lib.exceptions import (
37 37 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
38 38 )
39 39 from rhodecode.lib.utils2 import safe_unicode, safe_str
40 40 from rhodecode.model.db import User
41 41 from rhodecode.model.validators import Missing
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45 try:
46 46 import ldap
47 47 except ImportError:
48 48 # means that python-ldap is not installed, we use Missing object to mark
49 49 # ldap lib is Missing
50 50 ldap = Missing
51 51
52 52
53 53 def plugin_factory(plugin_id, *args, **kwds):
54 54 """
55 55 Factory function that is called during plugin discovery.
56 56 It returns the plugin instance.
57 57 """
58 58 plugin = RhodeCodeAuthPlugin(plugin_id)
59 59 return plugin
60 60
61 61
62 62 class LdapAuthnResource(AuthnPluginResourceBase):
63 63 pass
64 64
65 65
66 66 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
67 67 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
68 68 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
69 69 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
70 70
71 71 host = colander.SchemaNode(
72 72 colander.String(),
73 73 default='',
74 74 description=_('Host[s] of the LDAP Server \n'
75 75 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
76 76 'Multiple servers can be specified using commas'),
77 77 preparer=strip_whitespace,
78 78 title=_('LDAP Host'),
79 79 widget='string')
80 80 port = colander.SchemaNode(
81 81 colander.Int(),
82 82 default=389,
83 83 description=_('Custom port that the LDAP server is listening on. '
84 84 'Default value is: 389'),
85 85 preparer=strip_whitespace,
86 86 title=_('Port'),
87 87 validator=colander.Range(min=0, max=65536),
88 88 widget='int')
89 89 dn_user = colander.SchemaNode(
90 90 colander.String(),
91 91 default='',
92 92 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
93 93 'e.g., cn=admin,dc=mydomain,dc=com, or '
94 94 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
95 95 missing='',
96 96 preparer=strip_whitespace,
97 97 title=_('Account'),
98 98 widget='string')
99 99 dn_pass = colander.SchemaNode(
100 100 colander.String(),
101 101 default='',
102 102 description=_('Password to authenticate for given user DN.'),
103 103 missing='',
104 104 preparer=strip_whitespace,
105 105 title=_('Password'),
106 106 widget='password')
107 107 tls_kind = colander.SchemaNode(
108 108 colander.String(),
109 109 default=tls_kind_choices[0],
110 110 description=_('TLS Type'),
111 111 title=_('Connection Security'),
112 112 validator=colander.OneOf(tls_kind_choices),
113 113 widget='select')
114 114 tls_reqcert = colander.SchemaNode(
115 115 colander.String(),
116 116 default=tls_reqcert_choices[0],
117 117 description=_('Require Cert over TLS?. Self-signed and custom '
118 118 'certificates can be used when\n `RhodeCode Certificate` '
119 119 'found in admin > settings > system info page is extended.'),
120 120 title=_('Certificate Checks'),
121 121 validator=colander.OneOf(tls_reqcert_choices),
122 122 widget='select')
123 123 base_dn = colander.SchemaNode(
124 124 colander.String(),
125 125 default='',
126 126 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
127 127 'in it to be replaced with current user credentials \n'
128 128 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
129 129 missing='',
130 130 preparer=strip_whitespace,
131 131 title=_('Base DN'),
132 132 widget='string')
133 133 filter = colander.SchemaNode(
134 134 colander.String(),
135 135 default='',
136 136 description=_('Filter to narrow results \n'
137 137 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
138 138 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
139 139 missing='',
140 140 preparer=strip_whitespace,
141 141 title=_('LDAP Search Filter'),
142 142 widget='string')
143 143
144 144 search_scope = colander.SchemaNode(
145 145 colander.String(),
146 146 default=search_scope_choices[2],
147 147 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
148 148 title=_('LDAP Search Scope'),
149 149 validator=colander.OneOf(search_scope_choices),
150 150 widget='select')
151 151 attr_login = colander.SchemaNode(
152 152 colander.String(),
153 153 default='uid',
154 154 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
155 155 preparer=strip_whitespace,
156 156 title=_('Login Attribute'),
157 157 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
158 158 widget='string')
159 159 attr_firstname = colander.SchemaNode(
160 160 colander.String(),
161 161 default='',
162 162 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
163 163 missing='',
164 164 preparer=strip_whitespace,
165 165 title=_('First Name Attribute'),
166 166 widget='string')
167 167 attr_lastname = colander.SchemaNode(
168 168 colander.String(),
169 169 default='',
170 170 description=_('LDAP Attribute to map to last name (e.g., sn)'),
171 171 missing='',
172 172 preparer=strip_whitespace,
173 173 title=_('Last Name Attribute'),
174 174 widget='string')
175 175 attr_email = colander.SchemaNode(
176 176 colander.String(),
177 177 default='',
178 178 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
179 179 'Emails are a crucial part of RhodeCode. \n'
180 180 'If possible add a valid email attribute to ldap users.'),
181 181 missing='',
182 182 preparer=strip_whitespace,
183 183 title=_('Email Attribute'),
184 184 widget='string')
185 185
186 186
187 187 class AuthLdap(object):
188 188
189 189 def _build_servers(self):
190 190 return ', '.join(
191 191 ["{}://{}:{}".format(
192 192 self.ldap_server_type, host.strip(), self.LDAP_SERVER_PORT)
193 193 for host in self.SERVER_ADDRESSES])
194 194
195 195 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
196 196 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
197 197 search_scope='SUBTREE', attr_login='uid',
198 198 ldap_filter=None):
199 199 if ldap == Missing:
200 200 raise LdapImportError("Missing or incompatible ldap library")
201 201
202 202 self.debug = False
203 203 self.ldap_version = ldap_version
204 204 self.ldap_server_type = 'ldap'
205 205
206 206 self.TLS_KIND = tls_kind
207 207
208 208 if self.TLS_KIND == 'LDAPS':
209 209 port = port or 689
210 210 self.ldap_server_type += 's'
211 211
212 212 OPT_X_TLS_DEMAND = 2
213 213 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
214 214 OPT_X_TLS_DEMAND)
215 215 # split server into list
216 216 self.SERVER_ADDRESSES = server.split(',')
217 217 self.LDAP_SERVER_PORT = port
218 218
219 219 # USE FOR READ ONLY BIND TO LDAP SERVER
220 220 self.attr_login = attr_login
221 221
222 222 self.LDAP_BIND_DN = safe_str(bind_dn)
223 223 self.LDAP_BIND_PASS = safe_str(bind_pass)
224 224 self.LDAP_SERVER = self._build_servers()
225 225 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
226 226 self.BASE_DN = safe_str(base_dn)
227 227 self.LDAP_FILTER = safe_str(ldap_filter)
228 228
229 229 def _get_ldap_server(self):
230 230 if self.debug:
231 231 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
232 232 if hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
233 233 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR,
234 234 '/etc/openldap/cacerts')
235 235 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
236 236 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
237 237 ldap.set_option(ldap.OPT_TIMEOUT, 20)
238 238 ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 10)
239 239 ldap.set_option(ldap.OPT_TIMELIMIT, 15)
240 240 if self.TLS_KIND != 'PLAIN':
241 241 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
242 242 server = ldap.initialize(self.LDAP_SERVER)
243 243 if self.ldap_version == 2:
244 244 server.protocol = ldap.VERSION2
245 245 else:
246 246 server.protocol = ldap.VERSION3
247 247
248 248 if self.TLS_KIND == 'START_TLS':
249 249 server.start_tls_s()
250 250
251 251 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
252 252 log.debug('Trying simple_bind with password and given login DN: %s',
253 253 self.LDAP_BIND_DN)
254 254 server.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
255 255
256 256 return server
257 257
258 258 def get_uid(self, username):
259 259 uid = username
260 260 for server_addr in self.SERVER_ADDRESSES:
261 261 uid = chop_at(username, "@%s" % server_addr)
262 262 return uid
263 263
264 264 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
265 265 try:
266 266 log.debug('Trying simple bind with %s', dn)
267 267 server.simple_bind_s(dn, safe_str(password))
268 268 user = server.search_ext_s(
269 269 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
270 270 _, attrs = user
271 271 return attrs
272 272
273 273 except ldap.INVALID_CREDENTIALS:
274 274 log.debug(
275 275 "LDAP rejected password for user '%s': %s, org_exc:",
276 276 username, dn, exc_info=True)
277 277
278 278 def authenticate_ldap(self, username, password):
279 279 """
280 280 Authenticate a user via LDAP and return his/her LDAP properties.
281 281
282 282 Raises AuthenticationError if the credentials are rejected, or
283 283 EnvironmentError if the LDAP server can't be reached.
284 284
285 285 :param username: username
286 286 :param password: password
287 287 """
288 288
289 289 uid = self.get_uid(username)
290 290
291 291 if not password:
292 292 msg = "Authenticating user %s with blank password not allowed"
293 293 log.warning(msg, username)
294 294 raise LdapPasswordError(msg)
295 295 if "," in username:
296 296 raise LdapUsernameError(
297 297 "invalid character `,` in username: `{}`".format(username))
298 298 try:
299 299 server = self._get_ldap_server()
300 300 filter_ = '(&%s(%s=%s))' % (
301 301 self.LDAP_FILTER, self.attr_login, username)
302 302 log.debug("Authenticating %r filter %s at %s", self.BASE_DN,
303 303 filter_, self.LDAP_SERVER)
304 304 lobjects = server.search_ext_s(
305 305 self.BASE_DN, self.SEARCH_SCOPE, filter_)
306 306
307 307 if not lobjects:
308 308 log.debug("No matching LDAP objects for authentication "
309 309 "of UID:'%s' username:(%s)", uid, username)
310 310 raise ldap.NO_SUCH_OBJECT()
311 311
312 312 log.debug('Found matching ldap object, trying to authenticate')
313 313 for (dn, _attrs) in lobjects:
314 314 if dn is None:
315 315 continue
316 316
317 317 user_attrs = self.fetch_attrs_from_simple_bind(
318 318 server, dn, username, password)
319 319 if user_attrs:
320 320 break
321 321
322 322 else:
323 323 raise LdapPasswordError(
324 324 'Failed to authenticate user `{}`'
325 325 'with given password'.format(username))
326 326
327 327 except ldap.NO_SUCH_OBJECT:
328 328 log.debug("LDAP says no such user '%s' (%s), org_exc:",
329 329 uid, username, exc_info=True)
330 330 raise LdapUsernameError('Unable to find user')
331 331 except ldap.SERVER_DOWN:
332 332 org_exc = traceback.format_exc()
333 333 raise LdapConnectionError(
334 334 "LDAP can't access authentication "
335 335 "server, org_exc:%s" % org_exc)
336 336
337 337 return dn, user_attrs
338 338
339 339
340 340 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
341 341 # used to define dynamic binding in the
342 342 DYNAMIC_BIND_VAR = '$login'
343 343 _settings_unsafe_keys = ['dn_pass']
344 344
345 345 def includeme(self, config):
346 346 config.add_authn_plugin(self)
347 347 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
348 348 config.add_view(
349 349 'rhodecode.authentication.views.AuthnPluginViewBase',
350 350 attr='settings_get',
351 351 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
352 352 request_method='GET',
353 353 route_name='auth_home',
354 354 context=LdapAuthnResource)
355 355 config.add_view(
356 356 'rhodecode.authentication.views.AuthnPluginViewBase',
357 357 attr='settings_post',
358 358 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
359 359 request_method='POST',
360 360 route_name='auth_home',
361 361 context=LdapAuthnResource)
362 362
363 363 def get_settings_schema(self):
364 364 return LdapSettingsSchema()
365 365
366 366 def get_display_name(self):
367 367 return _('LDAP')
368 368
369 369 @hybrid_property
370 370 def name(self):
371 371 return "ldap"
372 372
373 373 def use_fake_password(self):
374 374 return True
375 375
376 376 def user_activation_state(self):
377 def_user_perms = User.get_default_user().AuthUser.permissions['global']
377 def_user_perms = User.get_default_user().AuthUser().permissions['global']
378 378 return 'hg.extern_activate.auto' in def_user_perms
379 379
380 380 def try_dynamic_binding(self, username, password, current_args):
381 381 """
382 382 Detects marker inside our original bind, and uses dynamic auth if
383 383 present
384 384 """
385 385
386 386 org_bind = current_args['bind_dn']
387 387 passwd = current_args['bind_pass']
388 388
389 389 def has_bind_marker(username):
390 390 if self.DYNAMIC_BIND_VAR in username:
391 391 return True
392 392
393 393 # we only passed in user with "special" variable
394 394 if org_bind and has_bind_marker(org_bind) and not passwd:
395 395 log.debug('Using dynamic user/password binding for ldap '
396 396 'authentication. Replacing `%s` with username',
397 397 self.DYNAMIC_BIND_VAR)
398 398 current_args['bind_dn'] = org_bind.replace(
399 399 self.DYNAMIC_BIND_VAR, username)
400 400 current_args['bind_pass'] = password
401 401
402 402 return current_args
403 403
404 404 def auth(self, userobj, username, password, settings, **kwargs):
405 405 """
406 406 Given a user object (which may be null), username, a plaintext password,
407 407 and a settings object (containing all the keys needed as listed in
408 408 settings()), authenticate this user's login attempt.
409 409
410 410 Return None on failure. On success, return a dictionary of the form:
411 411
412 412 see: RhodeCodeAuthPluginBase.auth_func_attrs
413 413 This is later validated for correctness
414 414 """
415 415
416 416 if not username or not password:
417 417 log.debug('Empty username or password skipping...')
418 418 return None
419 419
420 420 ldap_args = {
421 421 'server': settings.get('host', ''),
422 422 'base_dn': settings.get('base_dn', ''),
423 423 'port': settings.get('port'),
424 424 'bind_dn': settings.get('dn_user'),
425 425 'bind_pass': settings.get('dn_pass'),
426 426 'tls_kind': settings.get('tls_kind'),
427 427 'tls_reqcert': settings.get('tls_reqcert'),
428 428 'search_scope': settings.get('search_scope'),
429 429 'attr_login': settings.get('attr_login'),
430 430 'ldap_version': 3,
431 431 'ldap_filter': settings.get('filter'),
432 432 }
433 433
434 434 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
435 435
436 436 log.debug('Checking for ldap authentication.')
437 437
438 438 try:
439 439 aldap = AuthLdap(**ldap_args)
440 440 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
441 441 log.debug('Got ldap DN response %s', user_dn)
442 442
443 443 def get_ldap_attr(k):
444 444 return ldap_attrs.get(settings.get(k), [''])[0]
445 445
446 446 # old attrs fetched from RhodeCode database
447 447 admin = getattr(userobj, 'admin', False)
448 448 active = getattr(userobj, 'active', True)
449 449 email = getattr(userobj, 'email', '')
450 450 username = getattr(userobj, 'username', username)
451 451 firstname = getattr(userobj, 'firstname', '')
452 452 lastname = getattr(userobj, 'lastname', '')
453 453 extern_type = getattr(userobj, 'extern_type', '')
454 454
455 455 groups = []
456 456 user_attrs = {
457 457 'username': username,
458 458 'firstname': safe_unicode(
459 459 get_ldap_attr('attr_firstname') or firstname),
460 460 'lastname': safe_unicode(
461 461 get_ldap_attr('attr_lastname') or lastname),
462 462 'groups': groups,
463 463 'email': get_ldap_attr('attr_email') or email,
464 464 'admin': admin,
465 465 'active': active,
466 466 'active_from_extern': None,
467 467 'extern_name': user_dn,
468 468 'extern_type': extern_type,
469 469 }
470 470 log.debug('ldap user: %s', user_attrs)
471 471 log.info('user %s authenticated correctly', user_attrs['username'])
472 472
473 473 return user_attrs
474 474
475 475 except (LdapUsernameError, LdapPasswordError, LdapImportError):
476 476 log.exception("LDAP related exception")
477 477 return None
478 478 except (Exception,):
479 479 log.exception("Other exception")
480 480 return None
@@ -1,142 +1,142 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 RhodeCode authentication plugin for built in internal auth
23 23 """
24 24
25 25 import logging
26 26
27 27 from pylons.i18n.translation import lazy_ugettext as _
28 28
29 29 from rhodecode.authentication.base import RhodeCodeAuthPluginBase, hybrid_property
30 30 from rhodecode.authentication.routes import AuthnPluginResourceBase
31 31 from rhodecode.lib.utils2 import safe_str
32 32 from rhodecode.model.db import User
33 33
34 34 log = logging.getLogger(__name__)
35 35
36 36
37 37 def plugin_factory(plugin_id, *args, **kwds):
38 38 plugin = RhodeCodeAuthPlugin(plugin_id)
39 39 return plugin
40 40
41 41
42 42 class RhodecodeAuthnResource(AuthnPluginResourceBase):
43 43 pass
44 44
45 45
46 46 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
47 47
48 48 def includeme(self, config):
49 49 config.add_authn_plugin(self)
50 50 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
51 51 config.add_view(
52 52 'rhodecode.authentication.views.AuthnPluginViewBase',
53 53 attr='settings_get',
54 54 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
55 55 request_method='GET',
56 56 route_name='auth_home',
57 57 context=RhodecodeAuthnResource)
58 58 config.add_view(
59 59 'rhodecode.authentication.views.AuthnPluginViewBase',
60 60 attr='settings_post',
61 61 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
62 62 request_method='POST',
63 63 route_name='auth_home',
64 64 context=RhodecodeAuthnResource)
65 65
66 66 def get_display_name(self):
67 67 return _('Rhodecode')
68 68
69 69 @hybrid_property
70 70 def name(self):
71 71 return "rhodecode"
72 72
73 73 def user_activation_state(self):
74 def_user_perms = User.get_default_user().AuthUser.permissions['global']
74 def_user_perms = User.get_default_user().AuthUser().permissions['global']
75 75 return 'hg.register.auto_activate' in def_user_perms
76 76
77 77 def allows_authentication_from(
78 78 self, user, allows_non_existing_user=True,
79 79 allowed_auth_plugins=None, allowed_auth_sources=None):
80 80 """
81 81 Custom method for this auth that doesn't accept non existing users.
82 82 We know that user exists in our database.
83 83 """
84 84 allows_non_existing_user = False
85 85 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
86 86 user, allows_non_existing_user=allows_non_existing_user)
87 87
88 88 def auth(self, userobj, username, password, settings, **kwargs):
89 89 if not userobj:
90 90 log.debug('userobj was:%s skipping' % (userobj, ))
91 91 return None
92 92 if userobj.extern_type != self.name:
93 93 log.warning(
94 94 "userobj:%s extern_type mismatch got:`%s` expected:`%s`" %
95 95 (userobj, userobj.extern_type, self.name))
96 96 return None
97 97
98 98 user_attrs = {
99 99 "username": userobj.username,
100 100 "firstname": userobj.firstname,
101 101 "lastname": userobj.lastname,
102 102 "groups": [],
103 103 "email": userobj.email,
104 104 "admin": userobj.admin,
105 105 "active": userobj.active,
106 106 "active_from_extern": userobj.active,
107 107 "extern_name": userobj.user_id,
108 108 "extern_type": userobj.extern_type,
109 109 }
110 110
111 111 log.debug("User attributes:%s" % (user_attrs, ))
112 112 if userobj.active:
113 113 from rhodecode.lib import auth
114 114 crypto_backend = auth.crypto_backend()
115 115 password_encoded = safe_str(password)
116 116 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
117 117 password_encoded, userobj.password)
118 118
119 119 if password_match and new_hash:
120 120 log.debug('user %s properly authenticated, but '
121 121 'requires hash change to bcrypt', userobj)
122 122 # if password match, and we use OLD deprecated hash,
123 123 # we should migrate this user hash password to the new hash
124 124 # we store the new returned by hash_check_with_upgrade function
125 125 user_attrs['_hash_migrate'] = new_hash
126 126
127 127 if userobj.username == User.DEFAULT_USER and userobj.active:
128 128 log.info(
129 129 'user %s authenticated correctly as anonymous user', userobj)
130 130 return user_attrs
131 131
132 132 elif userobj.username == username and password_match:
133 133 log.info('user %s authenticated correctly', userobj)
134 134 return user_attrs
135 135 log.info("user %s had a bad password when "
136 136 "authenticating on this plugin", userobj)
137 137 return None
138 138 else:
139 139 log.warning(
140 140 'user `%s` failed to authenticate via %s, reason: account not '
141 141 'active.', username, self.name)
142 142 return None
@@ -1,146 +1,146 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 RhodeCode authentication token plugin for built in internal auth
23 23 """
24 24
25 25 import logging
26 26
27 27 from rhodecode.translation import _
28 28 from rhodecode.authentication.base import (
29 29 RhodeCodeAuthPluginBase, VCS_TYPE, hybrid_property)
30 30 from rhodecode.authentication.routes import AuthnPluginResourceBase
31 31 from rhodecode.model.db import User, UserApiKeys, Repository
32 32
33 33
34 34 log = logging.getLogger(__name__)
35 35
36 36
37 37 def plugin_factory(plugin_id, *args, **kwds):
38 38 plugin = RhodeCodeAuthPlugin(plugin_id)
39 39 return plugin
40 40
41 41
42 42 class RhodecodeAuthnResource(AuthnPluginResourceBase):
43 43 pass
44 44
45 45
46 46 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
47 47 """
48 48 Enables usage of authentication tokens for vcs operations.
49 49 """
50 50
51 51 def includeme(self, config):
52 52 config.add_authn_plugin(self)
53 53 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
54 54 config.add_view(
55 55 'rhodecode.authentication.views.AuthnPluginViewBase',
56 56 attr='settings_get',
57 57 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
58 58 request_method='GET',
59 59 route_name='auth_home',
60 60 context=RhodecodeAuthnResource)
61 61 config.add_view(
62 62 'rhodecode.authentication.views.AuthnPluginViewBase',
63 63 attr='settings_post',
64 64 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
65 65 request_method='POST',
66 66 route_name='auth_home',
67 67 context=RhodecodeAuthnResource)
68 68
69 69 def get_display_name(self):
70 70 return _('Rhodecode Token Auth')
71 71
72 72 @hybrid_property
73 73 def name(self):
74 74 return "authtoken"
75 75
76 76 def user_activation_state(self):
77 def_user_perms = User.get_default_user().AuthUser.permissions['global']
77 def_user_perms = User.get_default_user().AuthUser().permissions['global']
78 78 return 'hg.register.auto_activate' in def_user_perms
79 79
80 80 def allows_authentication_from(
81 81 self, user, allows_non_existing_user=True,
82 82 allowed_auth_plugins=None, allowed_auth_sources=None):
83 83 """
84 84 Custom method for this auth that doesn't accept empty users. And also
85 85 allows users from all other active plugins to use it and also
86 86 authenticate against it. But only via vcs mode
87 87 """
88 88 from rhodecode.authentication.base import get_authn_registry
89 89 authn_registry = get_authn_registry()
90 90
91 91 active_plugins = set(
92 92 [x.name for x in authn_registry.get_plugins_for_authentication()])
93 93 active_plugins.discard(self.name)
94 94
95 95 allowed_auth_plugins = [self.name] + list(active_plugins)
96 96 # only for vcs operations
97 97 allowed_auth_sources = [VCS_TYPE]
98 98
99 99 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
100 100 user, allows_non_existing_user=False,
101 101 allowed_auth_plugins=allowed_auth_plugins,
102 102 allowed_auth_sources=allowed_auth_sources)
103 103
104 104 def auth(self, userobj, username, password, settings, **kwargs):
105 105 if not userobj:
106 106 log.debug('userobj was:%s skipping' % (userobj, ))
107 107 return None
108 108
109 109 user_attrs = {
110 110 "username": userobj.username,
111 111 "firstname": userobj.firstname,
112 112 "lastname": userobj.lastname,
113 113 "groups": [],
114 114 "email": userobj.email,
115 115 "admin": userobj.admin,
116 116 "active": userobj.active,
117 117 "active_from_extern": userobj.active,
118 118 "extern_name": userobj.user_id,
119 119 "extern_type": userobj.extern_type,
120 120 }
121 121
122 122 log.debug('Authenticating user with args %s', user_attrs)
123 123 if userobj.active:
124 124 # calling context repo for token scopes
125 125 scope_repo_id = None
126 126 if self.acl_repo_name:
127 127 repo = Repository.get_by_repo_name(self.acl_repo_name)
128 128 scope_repo_id = repo.repo_id if repo else None
129 129
130 130 token_match = userobj.authenticate_by_token(
131 131 password, roles=[UserApiKeys.ROLE_VCS],
132 132 scope_repo_id=scope_repo_id)
133 133
134 134 if userobj.username == username and token_match:
135 135 log.info(
136 136 'user `%s` successfully authenticated via %s',
137 137 user_attrs['username'], self.name)
138 138 return user_attrs
139 139 log.error(
140 140 'user `%s` failed to authenticate via %s, reason: bad or '
141 141 'inactive token.', username, self.name)
142 142 else:
143 143 log.warning(
144 144 'user `%s` failed to authenticate via %s, reason: account not '
145 145 'active.', username, self.name)
146 146 return None
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,109 +1,109 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 External module for testing plugins
23 23
24 24 rhodecode.tests.auth_external_test
25 25
26 26 """
27 27 import logging
28 28 import traceback
29 29
30 30 from rhodecode.authentication.base import (
31 31 RhodeCodeExternalAuthPlugin, hybrid_property)
32 32 from rhodecode.model.db import User
33 33 from rhodecode.lib.ext_json import formatted_json
34 34
35 35 log = logging.getLogger(__name__)
36 36
37 37
38 38 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
39 39 def __init__(self):
40 40 self._logger = logging.getLogger(__name__)
41 41
42 42 @hybrid_property
43 43 def allows_creating_users(self):
44 44 return True
45 45
46 46 @hybrid_property
47 47 def name(self):
48 48 return "external_test"
49 49
50 50 def settings(self):
51 51 settings = [
52 52 ]
53 53 return settings
54 54
55 55 def use_fake_password(self):
56 56 return True
57 57
58 58 def user_activation_state(self):
59 def_user_perms = User.get_default_user().AuthUser.permissions['global']
59 def_user_perms = User.get_default_user().AuthUser().permissions['global']
60 60 return 'hg.extern_activate.auto' in def_user_perms
61 61
62 62 def auth(self, userobj, username, password, settings, **kwargs):
63 63 """
64 64 Given a user object (which may be null), username, a plaintext password,
65 65 and a settings object (containing all the keys needed as listed in settings()),
66 66 authenticate this user's login attempt.
67 67
68 68 Return None on failure. On success, return a dictionary of the form:
69 69
70 70 see: RhodeCodeAuthPluginBase.auth_func_attrs
71 71 This is later validated for correctness
72 72 """
73 73
74 74 if not username or not password:
75 75 log.debug('Empty username or password skipping...')
76 76 return None
77 77
78 78 try:
79 79 user_dn = username
80 80
81 81 # # old attrs fetched from RhodeCode database
82 82 admin = getattr(userobj, 'admin', False)
83 83 active = getattr(userobj, 'active', True)
84 84 email = getattr(userobj, 'email', '')
85 85 firstname = getattr(userobj, 'firstname', '')
86 86 lastname = getattr(userobj, 'lastname', '')
87 87 extern_type = getattr(userobj, 'extern_type', '')
88 88 #
89 89 user_attrs = {
90 90 'username': username,
91 91 'firstname': firstname,
92 92 'lastname': lastname,
93 93 'groups': [],
94 94 'email': '%s@rhodecode.com' % username,
95 95 'admin': admin,
96 96 'active': active,
97 97 "active_from_extern": None,
98 98 'extern_name': user_dn,
99 99 'extern_type': extern_type,
100 100 }
101 101
102 102 log.debug('EXTERNAL user: \n%s' % formatted_json(user_attrs))
103 103 log.info('user %s authenticated correctly' % user_attrs['username'])
104 104
105 105 return user_attrs
106 106
107 107 except (Exception,):
108 108 log.error(traceback.format_exc())
109 109 return None
General Comments 0
You need to be logged in to leave comments. Login now