##// 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
@@ -1,129 +1,129 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import pytest
22 import pytest
23
23
24 from rhodecode.model.repo import RepoModel
24 from rhodecode.model.repo import RepoModel
25 from rhodecode.api.tests.utils import (
25 from rhodecode.api.tests.utils import (
26 build_data, api_call, assert_ok, assert_error, jsonify)
26 build_data, api_call, assert_ok, assert_error, jsonify)
27 from rhodecode.model.db import User
27 from rhodecode.model.db import User
28
28
29
29
30 @pytest.mark.usefixtures("testuser_api", "app")
30 @pytest.mark.usefixtures("testuser_api", "app")
31 class TestGetRepos(object):
31 class TestGetRepos(object):
32 def test_api_get_repos(self):
32 def test_api_get_repos(self):
33 id_, params = build_data(self.apikey, 'get_repos')
33 id_, params = build_data(self.apikey, 'get_repos')
34 response = api_call(self.app, params)
34 response = api_call(self.app, params)
35
35
36 result = []
36 result = []
37 for repo in RepoModel().get_all():
37 for repo in RepoModel().get_all():
38 result.append(repo.get_api_data(include_secrets=True))
38 result.append(repo.get_api_data(include_secrets=True))
39 ret = jsonify(result)
39 ret = jsonify(result)
40
40
41 expected = ret
41 expected = ret
42 assert_ok(id_, expected, given=response.body)
42 assert_ok(id_, expected, given=response.body)
43
43
44 def test_api_get_repos_only_toplevel(self, user_util):
44 def test_api_get_repos_only_toplevel(self, user_util):
45 repo_group = user_util.create_repo_group(auto_cleanup=True)
45 repo_group = user_util.create_repo_group(auto_cleanup=True)
46 user_util.create_repo(parent=repo_group)
46 user_util.create_repo(parent=repo_group)
47
47
48 id_, params = build_data(self.apikey, 'get_repos', traverse=0)
48 id_, params = build_data(self.apikey, 'get_repos', traverse=0)
49 response = api_call(self.app, params)
49 response = api_call(self.app, params)
50
50
51 result = []
51 result = []
52 for repo in RepoModel().get_repos_for_root(root=None):
52 for repo in RepoModel().get_repos_for_root(root=None):
53 result.append(repo.get_api_data(include_secrets=True))
53 result.append(repo.get_api_data(include_secrets=True))
54 expected = jsonify(result)
54 expected = jsonify(result)
55
55
56 assert_ok(id_, expected, given=response.body)
56 assert_ok(id_, expected, given=response.body)
57
57
58 def test_api_get_repos_with_wrong_root(self):
58 def test_api_get_repos_with_wrong_root(self):
59 id_, params = build_data(self.apikey, 'get_repos', root='abracadabra')
59 id_, params = build_data(self.apikey, 'get_repos', root='abracadabra')
60 response = api_call(self.app, params)
60 response = api_call(self.app, params)
61
61
62 expected = 'Root repository group `abracadabra` does not exist'
62 expected = 'Root repository group `abracadabra` does not exist'
63 assert_error(id_, expected, given=response.body)
63 assert_error(id_, expected, given=response.body)
64
64
65 def test_api_get_repos_with_root(self, user_util):
65 def test_api_get_repos_with_root(self, user_util):
66 repo_group = user_util.create_repo_group(auto_cleanup=True)
66 repo_group = user_util.create_repo_group(auto_cleanup=True)
67 repo_group_name = repo_group.group_name
67 repo_group_name = repo_group.group_name
68
68
69 user_util.create_repo(parent=repo_group)
69 user_util.create_repo(parent=repo_group)
70 user_util.create_repo(parent=repo_group)
70 user_util.create_repo(parent=repo_group)
71
71
72 # nested, should not show up
72 # nested, should not show up
73 user_util._test_name = '{}/'.format(repo_group_name)
73 user_util._test_name = '{}/'.format(repo_group_name)
74 sub_repo_group = user_util.create_repo_group(auto_cleanup=True)
74 sub_repo_group = user_util.create_repo_group(auto_cleanup=True)
75 user_util.create_repo(parent=sub_repo_group)
75 user_util.create_repo(parent=sub_repo_group)
76
76
77 id_, params = build_data(self.apikey, 'get_repos',
77 id_, params = build_data(self.apikey, 'get_repos',
78 root=repo_group_name, traverse=0)
78 root=repo_group_name, traverse=0)
79 response = api_call(self.app, params)
79 response = api_call(self.app, params)
80
80
81 result = []
81 result = []
82 for repo in RepoModel().get_repos_for_root(repo_group):
82 for repo in RepoModel().get_repos_for_root(repo_group):
83 result.append(repo.get_api_data(include_secrets=True))
83 result.append(repo.get_api_data(include_secrets=True))
84
84
85 assert len(result) == 2
85 assert len(result) == 2
86 expected = jsonify(result)
86 expected = jsonify(result)
87 assert_ok(id_, expected, given=response.body)
87 assert_ok(id_, expected, given=response.body)
88
88
89 def test_api_get_repos_with_root_and_traverse(self, user_util):
89 def test_api_get_repos_with_root_and_traverse(self, user_util):
90 repo_group = user_util.create_repo_group(auto_cleanup=True)
90 repo_group = user_util.create_repo_group(auto_cleanup=True)
91 repo_group_name = repo_group.group_name
91 repo_group_name = repo_group.group_name
92
92
93 user_util.create_repo(parent=repo_group)
93 user_util.create_repo(parent=repo_group)
94 user_util.create_repo(parent=repo_group)
94 user_util.create_repo(parent=repo_group)
95
95
96 # nested, should not show up
96 # nested, should not show up
97 user_util._test_name = '{}/'.format(repo_group_name)
97 user_util._test_name = '{}/'.format(repo_group_name)
98 sub_repo_group = user_util.create_repo_group(auto_cleanup=True)
98 sub_repo_group = user_util.create_repo_group(auto_cleanup=True)
99 user_util.create_repo(parent=sub_repo_group)
99 user_util.create_repo(parent=sub_repo_group)
100
100
101 id_, params = build_data(self.apikey, 'get_repos',
101 id_, params = build_data(self.apikey, 'get_repos',
102 root=repo_group_name, traverse=1)
102 root=repo_group_name, traverse=1)
103 response = api_call(self.app, params)
103 response = api_call(self.app, params)
104
104
105 result = []
105 result = []
106 for repo in RepoModel().get_repos_for_root(
106 for repo in RepoModel().get_repos_for_root(
107 repo_group_name, traverse=True):
107 repo_group_name, traverse=True):
108 result.append(repo.get_api_data(include_secrets=True))
108 result.append(repo.get_api_data(include_secrets=True))
109
109
110 assert len(result) == 3
110 assert len(result) == 3
111 expected = jsonify(result)
111 expected = jsonify(result)
112 assert_ok(id_, expected, given=response.body)
112 assert_ok(id_, expected, given=response.body)
113
113
114 def test_api_get_repos_non_admin(self):
114 def test_api_get_repos_non_admin(self):
115 id_, params = build_data(self.apikey_regular, 'get_repos')
115 id_, params = build_data(self.apikey_regular, 'get_repos')
116 response = api_call(self.app, params)
116 response = api_call(self.app, params)
117
117
118 user = User.get_by_username(self.TEST_USER_LOGIN)
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 result = []
121 result = []
122 for repo in RepoModel().get_all():
122 for repo in RepoModel().get_all():
123 perm = allowed_repos[repo.repo_name]
123 perm = allowed_repos[repo.repo_name]
124 if perm in ['repository.read', 'repository.write', 'repository.admin']:
124 if perm in ['repository.read', 'repository.write', 'repository.admin']:
125 result.append(repo.get_api_data())
125 result.append(repo.get_api_data())
126 ret = jsonify(result)
126 ret = jsonify(result)
127
127
128 expected = ret
128 expected = ret
129 assert_ok(id_, expected, given=response.body)
129 assert_ok(id_, expected, given=response.body)
@@ -1,369 +1,369 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import re
21 import re
22 import logging
22 import logging
23 import formencode
23 import formencode
24 from pyramid.interfaces import IRoutesMapper
24 from pyramid.interfaces import IRoutesMapper
25
25
26 from pyramid.view import view_config
26 from pyramid.view import view_config
27 from pyramid.httpexceptions import HTTPFound
27 from pyramid.httpexceptions import HTTPFound
28 from pyramid.renderers import render
28 from pyramid.renderers import render
29 from pyramid.response import Response
29 from pyramid.response import Response
30
30
31 from rhodecode.apps._base import BaseAppView
31 from rhodecode.apps._base import BaseAppView
32
32
33 from rhodecode.lib import helpers as h
33 from rhodecode.lib import helpers as h
34 from rhodecode.lib.auth import (
34 from rhodecode.lib.auth import (
35 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
35 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
36 from rhodecode.lib.utils2 import aslist
36 from rhodecode.lib.utils2 import aslist
37 from rhodecode.model.db import User, UserIpMap
37 from rhodecode.model.db import User, UserIpMap
38 from rhodecode.model.forms import (
38 from rhodecode.model.forms import (
39 ApplicationPermissionsForm, ObjectPermissionsForm, UserPermissionsForm)
39 ApplicationPermissionsForm, ObjectPermissionsForm, UserPermissionsForm)
40 from rhodecode.model.meta import Session
40 from rhodecode.model.meta import Session
41 from rhodecode.model.permission import PermissionModel
41 from rhodecode.model.permission import PermissionModel
42 from rhodecode.model.settings import SettingsModel
42 from rhodecode.model.settings import SettingsModel
43
43
44
44
45 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
46
46
47
47
48 class AdminPermissionsView(BaseAppView):
48 class AdminPermissionsView(BaseAppView):
49 def load_default_context(self):
49 def load_default_context(self):
50 c = self._get_local_tmpl_context()
50 c = self._get_local_tmpl_context()
51
51
52 self._register_global_c(c)
52 self._register_global_c(c)
53 PermissionModel().set_global_permission_choices(
53 PermissionModel().set_global_permission_choices(
54 c, gettext_translator=self.request.translate)
54 c, gettext_translator=self.request.translate)
55 return c
55 return c
56
56
57 @LoginRequired()
57 @LoginRequired()
58 @HasPermissionAllDecorator('hg.admin')
58 @HasPermissionAllDecorator('hg.admin')
59 @view_config(
59 @view_config(
60 route_name='admin_permissions_application', request_method='GET',
60 route_name='admin_permissions_application', request_method='GET',
61 renderer='rhodecode:templates/admin/permissions/permissions.mako')
61 renderer='rhodecode:templates/admin/permissions/permissions.mako')
62 def permissions_application(self):
62 def permissions_application(self):
63 c = self.load_default_context()
63 c = self.load_default_context()
64 c.active = 'application'
64 c.active = 'application'
65
65
66 c.user = User.get_default_user(refresh=True)
66 c.user = User.get_default_user(refresh=True)
67
67
68 app_settings = SettingsModel().get_all_settings()
68 app_settings = SettingsModel().get_all_settings()
69 defaults = {
69 defaults = {
70 'anonymous': c.user.active,
70 'anonymous': c.user.active,
71 'default_register_message': app_settings.get(
71 'default_register_message': app_settings.get(
72 'rhodecode_register_message')
72 'rhodecode_register_message')
73 }
73 }
74 defaults.update(c.user.get_default_perms())
74 defaults.update(c.user.get_default_perms())
75
75
76 data = render('rhodecode:templates/admin/permissions/permissions.mako',
76 data = render('rhodecode:templates/admin/permissions/permissions.mako',
77 self._get_template_context(c), self.request)
77 self._get_template_context(c), self.request)
78 html = formencode.htmlfill.render(
78 html = formencode.htmlfill.render(
79 data,
79 data,
80 defaults=defaults,
80 defaults=defaults,
81 encoding="UTF-8",
81 encoding="UTF-8",
82 force_defaults=False
82 force_defaults=False
83 )
83 )
84 return Response(html)
84 return Response(html)
85
85
86 @LoginRequired()
86 @LoginRequired()
87 @HasPermissionAllDecorator('hg.admin')
87 @HasPermissionAllDecorator('hg.admin')
88 @CSRFRequired()
88 @CSRFRequired()
89 @view_config(
89 @view_config(
90 route_name='admin_permissions_application_update', request_method='POST',
90 route_name='admin_permissions_application_update', request_method='POST',
91 renderer='rhodecode:templates/admin/permissions/permissions.mako')
91 renderer='rhodecode:templates/admin/permissions/permissions.mako')
92 def permissions_application_update(self):
92 def permissions_application_update(self):
93 _ = self.request.translate
93 _ = self.request.translate
94 c = self.load_default_context()
94 c = self.load_default_context()
95 c.active = 'application'
95 c.active = 'application'
96
96
97 _form = ApplicationPermissionsForm(
97 _form = ApplicationPermissionsForm(
98 [x[0] for x in c.register_choices],
98 [x[0] for x in c.register_choices],
99 [x[0] for x in c.password_reset_choices],
99 [x[0] for x in c.password_reset_choices],
100 [x[0] for x in c.extern_activate_choices])()
100 [x[0] for x in c.extern_activate_choices])()
101
101
102 try:
102 try:
103 form_result = _form.to_python(dict(self.request.POST))
103 form_result = _form.to_python(dict(self.request.POST))
104 form_result.update({'perm_user_name': User.DEFAULT_USER})
104 form_result.update({'perm_user_name': User.DEFAULT_USER})
105 PermissionModel().update_application_permissions(form_result)
105 PermissionModel().update_application_permissions(form_result)
106
106
107 settings = [
107 settings = [
108 ('register_message', 'default_register_message'),
108 ('register_message', 'default_register_message'),
109 ]
109 ]
110 for setting, form_key in settings:
110 for setting, form_key in settings:
111 sett = SettingsModel().create_or_update_setting(
111 sett = SettingsModel().create_or_update_setting(
112 setting, form_result[form_key])
112 setting, form_result[form_key])
113 Session().add(sett)
113 Session().add(sett)
114
114
115 Session().commit()
115 Session().commit()
116 h.flash(_('Application permissions updated successfully'),
116 h.flash(_('Application permissions updated successfully'),
117 category='success')
117 category='success')
118
118
119 except formencode.Invalid as errors:
119 except formencode.Invalid as errors:
120 defaults = errors.value
120 defaults = errors.value
121
121
122 data = render(
122 data = render(
123 'rhodecode:templates/admin/permissions/permissions.mako',
123 'rhodecode:templates/admin/permissions/permissions.mako',
124 self._get_template_context(c), self.request)
124 self._get_template_context(c), self.request)
125 html = formencode.htmlfill.render(
125 html = formencode.htmlfill.render(
126 data,
126 data,
127 defaults=defaults,
127 defaults=defaults,
128 errors=errors.error_dict or {},
128 errors=errors.error_dict or {},
129 prefix_error=False,
129 prefix_error=False,
130 encoding="UTF-8",
130 encoding="UTF-8",
131 force_defaults=False
131 force_defaults=False
132 )
132 )
133 return Response(html)
133 return Response(html)
134
134
135 except Exception:
135 except Exception:
136 log.exception("Exception during update of permissions")
136 log.exception("Exception during update of permissions")
137 h.flash(_('Error occurred during update of permissions'),
137 h.flash(_('Error occurred during update of permissions'),
138 category='error')
138 category='error')
139
139
140 raise HTTPFound(h.route_path('admin_permissions_application'))
140 raise HTTPFound(h.route_path('admin_permissions_application'))
141
141
142 @LoginRequired()
142 @LoginRequired()
143 @HasPermissionAllDecorator('hg.admin')
143 @HasPermissionAllDecorator('hg.admin')
144 @view_config(
144 @view_config(
145 route_name='admin_permissions_object', request_method='GET',
145 route_name='admin_permissions_object', request_method='GET',
146 renderer='rhodecode:templates/admin/permissions/permissions.mako')
146 renderer='rhodecode:templates/admin/permissions/permissions.mako')
147 def permissions_objects(self):
147 def permissions_objects(self):
148 c = self.load_default_context()
148 c = self.load_default_context()
149 c.active = 'objects'
149 c.active = 'objects'
150
150
151 c.user = User.get_default_user(refresh=True)
151 c.user = User.get_default_user(refresh=True)
152 defaults = {}
152 defaults = {}
153 defaults.update(c.user.get_default_perms())
153 defaults.update(c.user.get_default_perms())
154
154
155 data = render(
155 data = render(
156 'rhodecode:templates/admin/permissions/permissions.mako',
156 'rhodecode:templates/admin/permissions/permissions.mako',
157 self._get_template_context(c), self.request)
157 self._get_template_context(c), self.request)
158 html = formencode.htmlfill.render(
158 html = formencode.htmlfill.render(
159 data,
159 data,
160 defaults=defaults,
160 defaults=defaults,
161 encoding="UTF-8",
161 encoding="UTF-8",
162 force_defaults=False
162 force_defaults=False
163 )
163 )
164 return Response(html)
164 return Response(html)
165
165
166 @LoginRequired()
166 @LoginRequired()
167 @HasPermissionAllDecorator('hg.admin')
167 @HasPermissionAllDecorator('hg.admin')
168 @CSRFRequired()
168 @CSRFRequired()
169 @view_config(
169 @view_config(
170 route_name='admin_permissions_object_update', request_method='POST',
170 route_name='admin_permissions_object_update', request_method='POST',
171 renderer='rhodecode:templates/admin/permissions/permissions.mako')
171 renderer='rhodecode:templates/admin/permissions/permissions.mako')
172 def permissions_objects_update(self):
172 def permissions_objects_update(self):
173 _ = self.request.translate
173 _ = self.request.translate
174 c = self.load_default_context()
174 c = self.load_default_context()
175 c.active = 'objects'
175 c.active = 'objects'
176
176
177 _form = ObjectPermissionsForm(
177 _form = ObjectPermissionsForm(
178 [x[0] for x in c.repo_perms_choices],
178 [x[0] for x in c.repo_perms_choices],
179 [x[0] for x in c.group_perms_choices],
179 [x[0] for x in c.group_perms_choices],
180 [x[0] for x in c.user_group_perms_choices])()
180 [x[0] for x in c.user_group_perms_choices])()
181
181
182 try:
182 try:
183 form_result = _form.to_python(dict(self.request.POST))
183 form_result = _form.to_python(dict(self.request.POST))
184 form_result.update({'perm_user_name': User.DEFAULT_USER})
184 form_result.update({'perm_user_name': User.DEFAULT_USER})
185 PermissionModel().update_object_permissions(form_result)
185 PermissionModel().update_object_permissions(form_result)
186
186
187 Session().commit()
187 Session().commit()
188 h.flash(_('Object permissions updated successfully'),
188 h.flash(_('Object permissions updated successfully'),
189 category='success')
189 category='success')
190
190
191 except formencode.Invalid as errors:
191 except formencode.Invalid as errors:
192 defaults = errors.value
192 defaults = errors.value
193
193
194 data = render(
194 data = render(
195 'rhodecode:templates/admin/permissions/permissions.mako',
195 'rhodecode:templates/admin/permissions/permissions.mako',
196 self._get_template_context(c), self.request)
196 self._get_template_context(c), self.request)
197 html = formencode.htmlfill.render(
197 html = formencode.htmlfill.render(
198 data,
198 data,
199 defaults=defaults,
199 defaults=defaults,
200 errors=errors.error_dict or {},
200 errors=errors.error_dict or {},
201 prefix_error=False,
201 prefix_error=False,
202 encoding="UTF-8",
202 encoding="UTF-8",
203 force_defaults=False
203 force_defaults=False
204 )
204 )
205 return Response(html)
205 return Response(html)
206 except Exception:
206 except Exception:
207 log.exception("Exception during update of permissions")
207 log.exception("Exception during update of permissions")
208 h.flash(_('Error occurred during update of permissions'),
208 h.flash(_('Error occurred during update of permissions'),
209 category='error')
209 category='error')
210
210
211 raise HTTPFound(h.route_path('admin_permissions_object'))
211 raise HTTPFound(h.route_path('admin_permissions_object'))
212
212
213 @LoginRequired()
213 @LoginRequired()
214 @HasPermissionAllDecorator('hg.admin')
214 @HasPermissionAllDecorator('hg.admin')
215 @view_config(
215 @view_config(
216 route_name='admin_permissions_global', request_method='GET',
216 route_name='admin_permissions_global', request_method='GET',
217 renderer='rhodecode:templates/admin/permissions/permissions.mako')
217 renderer='rhodecode:templates/admin/permissions/permissions.mako')
218 def permissions_global(self):
218 def permissions_global(self):
219 c = self.load_default_context()
219 c = self.load_default_context()
220 c.active = 'global'
220 c.active = 'global'
221
221
222 c.user = User.get_default_user(refresh=True)
222 c.user = User.get_default_user(refresh=True)
223 defaults = {}
223 defaults = {}
224 defaults.update(c.user.get_default_perms())
224 defaults.update(c.user.get_default_perms())
225
225
226 data = render(
226 data = render(
227 'rhodecode:templates/admin/permissions/permissions.mako',
227 'rhodecode:templates/admin/permissions/permissions.mako',
228 self._get_template_context(c), self.request)
228 self._get_template_context(c), self.request)
229 html = formencode.htmlfill.render(
229 html = formencode.htmlfill.render(
230 data,
230 data,
231 defaults=defaults,
231 defaults=defaults,
232 encoding="UTF-8",
232 encoding="UTF-8",
233 force_defaults=False
233 force_defaults=False
234 )
234 )
235 return Response(html)
235 return Response(html)
236
236
237 @LoginRequired()
237 @LoginRequired()
238 @HasPermissionAllDecorator('hg.admin')
238 @HasPermissionAllDecorator('hg.admin')
239 @CSRFRequired()
239 @CSRFRequired()
240 @view_config(
240 @view_config(
241 route_name='admin_permissions_global_update', request_method='POST',
241 route_name='admin_permissions_global_update', request_method='POST',
242 renderer='rhodecode:templates/admin/permissions/permissions.mako')
242 renderer='rhodecode:templates/admin/permissions/permissions.mako')
243 def permissions_global_update(self):
243 def permissions_global_update(self):
244 _ = self.request.translate
244 _ = self.request.translate
245 c = self.load_default_context()
245 c = self.load_default_context()
246 c.active = 'global'
246 c.active = 'global'
247
247
248 _form = UserPermissionsForm(
248 _form = UserPermissionsForm(
249 [x[0] for x in c.repo_create_choices],
249 [x[0] for x in c.repo_create_choices],
250 [x[0] for x in c.repo_create_on_write_choices],
250 [x[0] for x in c.repo_create_on_write_choices],
251 [x[0] for x in c.repo_group_create_choices],
251 [x[0] for x in c.repo_group_create_choices],
252 [x[0] for x in c.user_group_create_choices],
252 [x[0] for x in c.user_group_create_choices],
253 [x[0] for x in c.fork_choices],
253 [x[0] for x in c.fork_choices],
254 [x[0] for x in c.inherit_default_permission_choices])()
254 [x[0] for x in c.inherit_default_permission_choices])()
255
255
256 try:
256 try:
257 form_result = _form.to_python(dict(self.request.POST))
257 form_result = _form.to_python(dict(self.request.POST))
258 form_result.update({'perm_user_name': User.DEFAULT_USER})
258 form_result.update({'perm_user_name': User.DEFAULT_USER})
259 PermissionModel().update_user_permissions(form_result)
259 PermissionModel().update_user_permissions(form_result)
260
260
261 Session().commit()
261 Session().commit()
262 h.flash(_('Global permissions updated successfully'),
262 h.flash(_('Global permissions updated successfully'),
263 category='success')
263 category='success')
264
264
265 except formencode.Invalid as errors:
265 except formencode.Invalid as errors:
266 defaults = errors.value
266 defaults = errors.value
267
267
268 data = render(
268 data = render(
269 'rhodecode:templates/admin/permissions/permissions.mako',
269 'rhodecode:templates/admin/permissions/permissions.mako',
270 self._get_template_context(c), self.request)
270 self._get_template_context(c), self.request)
271 html = formencode.htmlfill.render(
271 html = formencode.htmlfill.render(
272 data,
272 data,
273 defaults=defaults,
273 defaults=defaults,
274 errors=errors.error_dict or {},
274 errors=errors.error_dict or {},
275 prefix_error=False,
275 prefix_error=False,
276 encoding="UTF-8",
276 encoding="UTF-8",
277 force_defaults=False
277 force_defaults=False
278 )
278 )
279 return Response(html)
279 return Response(html)
280 except Exception:
280 except Exception:
281 log.exception("Exception during update of permissions")
281 log.exception("Exception during update of permissions")
282 h.flash(_('Error occurred during update of permissions'),
282 h.flash(_('Error occurred during update of permissions'),
283 category='error')
283 category='error')
284
284
285 raise HTTPFound(h.route_path('admin_permissions_global'))
285 raise HTTPFound(h.route_path('admin_permissions_global'))
286
286
287 @LoginRequired()
287 @LoginRequired()
288 @HasPermissionAllDecorator('hg.admin')
288 @HasPermissionAllDecorator('hg.admin')
289 @view_config(
289 @view_config(
290 route_name='admin_permissions_ips', request_method='GET',
290 route_name='admin_permissions_ips', request_method='GET',
291 renderer='rhodecode:templates/admin/permissions/permissions.mako')
291 renderer='rhodecode:templates/admin/permissions/permissions.mako')
292 def permissions_ips(self):
292 def permissions_ips(self):
293 c = self.load_default_context()
293 c = self.load_default_context()
294 c.active = 'ips'
294 c.active = 'ips'
295
295
296 c.user = User.get_default_user(refresh=True)
296 c.user = User.get_default_user(refresh=True)
297 c.user_ip_map = (
297 c.user_ip_map = (
298 UserIpMap.query().filter(UserIpMap.user == c.user).all())
298 UserIpMap.query().filter(UserIpMap.user == c.user).all())
299
299
300 return self._get_template_context(c)
300 return self._get_template_context(c)
301
301
302 @LoginRequired()
302 @LoginRequired()
303 @HasPermissionAllDecorator('hg.admin')
303 @HasPermissionAllDecorator('hg.admin')
304 @view_config(
304 @view_config(
305 route_name='admin_permissions_overview', request_method='GET',
305 route_name='admin_permissions_overview', request_method='GET',
306 renderer='rhodecode:templates/admin/permissions/permissions.mako')
306 renderer='rhodecode:templates/admin/permissions/permissions.mako')
307 def permissions_overview(self):
307 def permissions_overview(self):
308 c = self.load_default_context()
308 c = self.load_default_context()
309 c.active = 'perms'
309 c.active = 'perms'
310
310
311 c.user = User.get_default_user(refresh=True)
311 c.user = User.get_default_user(refresh=True)
312 c.perm_user = c.user.AuthUser
312 c.perm_user = c.user.AuthUser()
313 return self._get_template_context(c)
313 return self._get_template_context(c)
314
314
315 @LoginRequired()
315 @LoginRequired()
316 @HasPermissionAllDecorator('hg.admin')
316 @HasPermissionAllDecorator('hg.admin')
317 @view_config(
317 @view_config(
318 route_name='admin_permissions_auth_token_access', request_method='GET',
318 route_name='admin_permissions_auth_token_access', request_method='GET',
319 renderer='rhodecode:templates/admin/permissions/permissions.mako')
319 renderer='rhodecode:templates/admin/permissions/permissions.mako')
320 def auth_token_access(self):
320 def auth_token_access(self):
321 from rhodecode import CONFIG
321 from rhodecode import CONFIG
322
322
323 c = self.load_default_context()
323 c = self.load_default_context()
324 c.active = 'auth_token_access'
324 c.active = 'auth_token_access'
325
325
326 c.user = User.get_default_user(refresh=True)
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 mapper = self.request.registry.queryUtility(IRoutesMapper)
329 mapper = self.request.registry.queryUtility(IRoutesMapper)
330 c.view_data = []
330 c.view_data = []
331
331
332 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
332 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
333 introspector = self.request.registry.introspector
333 introspector = self.request.registry.introspector
334
334
335 view_intr = {}
335 view_intr = {}
336 for view_data in introspector.get_category('views'):
336 for view_data in introspector.get_category('views'):
337 intr = view_data['introspectable']
337 intr = view_data['introspectable']
338
338
339 if 'route_name' in intr and intr['attr']:
339 if 'route_name' in intr and intr['attr']:
340 view_intr[intr['route_name']] = '{}:{}'.format(
340 view_intr[intr['route_name']] = '{}:{}'.format(
341 str(intr['derived_callable'].func_name), intr['attr']
341 str(intr['derived_callable'].func_name), intr['attr']
342 )
342 )
343
343
344 c.whitelist_key = 'api_access_controllers_whitelist'
344 c.whitelist_key = 'api_access_controllers_whitelist'
345 c.whitelist_file = CONFIG.get('__file__')
345 c.whitelist_file = CONFIG.get('__file__')
346 whitelist_views = aslist(
346 whitelist_views = aslist(
347 CONFIG.get(c.whitelist_key), sep=',')
347 CONFIG.get(c.whitelist_key), sep=',')
348
348
349 for route_info in mapper.get_routes():
349 for route_info in mapper.get_routes():
350 if not route_info.name.startswith('__'):
350 if not route_info.name.startswith('__'):
351 routepath = route_info.pattern
351 routepath = route_info.pattern
352
352
353 def replace(matchobj):
353 def replace(matchobj):
354 if matchobj.group(1):
354 if matchobj.group(1):
355 return "{%s}" % matchobj.group(1).split(':')[0]
355 return "{%s}" % matchobj.group(1).split(':')[0]
356 else:
356 else:
357 return "{%s}" % matchobj.group(2)
357 return "{%s}" % matchobj.group(2)
358
358
359 routepath = _argument_prog.sub(replace, routepath)
359 routepath = _argument_prog.sub(replace, routepath)
360
360
361 if not routepath.startswith('/'):
361 if not routepath.startswith('/'):
362 routepath = '/' + routepath
362 routepath = '/' + routepath
363
363
364 view_fqn = view_intr.get(route_info.name, 'NOT AVAILABLE')
364 view_fqn = view_intr.get(route_info.name, 'NOT AVAILABLE')
365 active = view_fqn in whitelist_views
365 active = view_fqn in whitelist_views
366 c.view_data.append((route_info.name, view_fqn, routepath, active))
366 c.view_data.append((route_info.name, view_fqn, routepath, active))
367
367
368 c.whitelist_views = whitelist_views
368 c.whitelist_views = whitelist_views
369 return self._get_template_context(c)
369 return self._get_template_context(c)
@@ -1,133 +1,133 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import mock
22 import mock
23 import pytest
23 import pytest
24
24
25 from rhodecode.apps.login.views import LoginView, CaptchaData
25 from rhodecode.apps.login.views import LoginView, CaptchaData
26 from rhodecode.config.routing import ADMIN_PREFIX
26 from rhodecode.config.routing import ADMIN_PREFIX
27 from rhodecode.lib.utils2 import AttributeDict
27 from rhodecode.lib.utils2 import AttributeDict
28 from rhodecode.model.settings import SettingsModel
28 from rhodecode.model.settings import SettingsModel
29 from rhodecode.tests.utils import AssertResponse
29 from rhodecode.tests.utils import AssertResponse
30
30
31
31
32 class RhodeCodeSetting(object):
32 class RhodeCodeSetting(object):
33 def __init__(self, name, value):
33 def __init__(self, name, value):
34 self.name = name
34 self.name = name
35 self.value = value
35 self.value = value
36
36
37 def __enter__(self):
37 def __enter__(self):
38 from rhodecode.model.settings import SettingsModel
38 from rhodecode.model.settings import SettingsModel
39 model = SettingsModel()
39 model = SettingsModel()
40 self.old_setting = model.get_setting_by_name(self.name)
40 self.old_setting = model.get_setting_by_name(self.name)
41 model.create_or_update_setting(name=self.name, val=self.value)
41 model.create_or_update_setting(name=self.name, val=self.value)
42 return self
42 return self
43
43
44 def __exit__(self, exc_type, exc_val, exc_tb):
44 def __exit__(self, exc_type, exc_val, exc_tb):
45 model = SettingsModel()
45 model = SettingsModel()
46 if self.old_setting:
46 if self.old_setting:
47 model.create_or_update_setting(
47 model.create_or_update_setting(
48 name=self.name, val=self.old_setting.app_settings_value)
48 name=self.name, val=self.old_setting.app_settings_value)
49 else:
49 else:
50 model.create_or_update_setting(name=self.name)
50 model.create_or_update_setting(name=self.name)
51
51
52
52
53 class TestRegisterCaptcha(object):
53 class TestRegisterCaptcha(object):
54
54
55 @pytest.mark.parametrize('private_key, public_key, expected', [
55 @pytest.mark.parametrize('private_key, public_key, expected', [
56 ('', '', CaptchaData(False, '', '')),
56 ('', '', CaptchaData(False, '', '')),
57 ('', 'pubkey', CaptchaData(False, '', 'pubkey')),
57 ('', 'pubkey', CaptchaData(False, '', 'pubkey')),
58 ('privkey', '', CaptchaData(True, 'privkey', '')),
58 ('privkey', '', CaptchaData(True, 'privkey', '')),
59 ('privkey', 'pubkey', CaptchaData(True, 'privkey', 'pubkey')),
59 ('privkey', 'pubkey', CaptchaData(True, 'privkey', 'pubkey')),
60 ])
60 ])
61 def test_get_captcha_data(self, private_key, public_key, expected, db,
61 def test_get_captcha_data(self, private_key, public_key, expected, db,
62 request_stub, user_util):
62 request_stub, user_util):
63 request_stub.user = user_util.create_user().AuthUser
63 request_stub.user = user_util.create_user().AuthUser()
64 request_stub.matched_route = AttributeDict({'name': 'login'})
64 request_stub.matched_route = AttributeDict({'name': 'login'})
65 login_view = LoginView(mock.Mock(), request_stub)
65 login_view = LoginView(mock.Mock(), request_stub)
66
66
67 with RhodeCodeSetting('captcha_private_key', private_key):
67 with RhodeCodeSetting('captcha_private_key', private_key):
68 with RhodeCodeSetting('captcha_public_key', public_key):
68 with RhodeCodeSetting('captcha_public_key', public_key):
69 captcha = login_view._get_captcha_data()
69 captcha = login_view._get_captcha_data()
70 assert captcha == expected
70 assert captcha == expected
71
71
72 @pytest.mark.parametrize('active', [False, True])
72 @pytest.mark.parametrize('active', [False, True])
73 @mock.patch.object(LoginView, '_get_captcha_data')
73 @mock.patch.object(LoginView, '_get_captcha_data')
74 def test_private_key_does_not_leak_to_html(
74 def test_private_key_does_not_leak_to_html(
75 self, m_get_captcha_data, active, app):
75 self, m_get_captcha_data, active, app):
76 captcha = CaptchaData(
76 captcha = CaptchaData(
77 active=active, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
77 active=active, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
78 m_get_captcha_data.return_value = captcha
78 m_get_captcha_data.return_value = captcha
79
79
80 response = app.get(ADMIN_PREFIX + '/register')
80 response = app.get(ADMIN_PREFIX + '/register')
81 assert 'PRIVATE_KEY' not in response
81 assert 'PRIVATE_KEY' not in response
82
82
83 @pytest.mark.parametrize('active', [False, True])
83 @pytest.mark.parametrize('active', [False, True])
84 @mock.patch.object(LoginView, '_get_captcha_data')
84 @mock.patch.object(LoginView, '_get_captcha_data')
85 def test_register_view_renders_captcha(
85 def test_register_view_renders_captcha(
86 self, m_get_captcha_data, active, app):
86 self, m_get_captcha_data, active, app):
87 captcha = CaptchaData(
87 captcha = CaptchaData(
88 active=active, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
88 active=active, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
89 m_get_captcha_data.return_value = captcha
89 m_get_captcha_data.return_value = captcha
90
90
91 response = app.get(ADMIN_PREFIX + '/register')
91 response = app.get(ADMIN_PREFIX + '/register')
92
92
93 assertr = AssertResponse(response)
93 assertr = AssertResponse(response)
94 if active:
94 if active:
95 assertr.one_element_exists('#recaptcha_field')
95 assertr.one_element_exists('#recaptcha_field')
96 else:
96 else:
97 assertr.no_element_exists('#recaptcha_field')
97 assertr.no_element_exists('#recaptcha_field')
98
98
99 @pytest.mark.parametrize('valid', [False, True])
99 @pytest.mark.parametrize('valid', [False, True])
100 @mock.patch('rhodecode.apps.login.views.submit')
100 @mock.patch('rhodecode.apps.login.views.submit')
101 @mock.patch.object(LoginView, '_get_captcha_data')
101 @mock.patch.object(LoginView, '_get_captcha_data')
102 def test_register_with_active_captcha(
102 def test_register_with_active_captcha(
103 self, m_get_captcha_data, m_submit, valid, app, csrf_token):
103 self, m_get_captcha_data, m_submit, valid, app, csrf_token):
104 captcha = CaptchaData(
104 captcha = CaptchaData(
105 active=True, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
105 active=True, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
106 m_get_captcha_data.return_value = captcha
106 m_get_captcha_data.return_value = captcha
107 m_response = mock.Mock()
107 m_response = mock.Mock()
108 m_response.is_valid = valid
108 m_response.is_valid = valid
109 m_submit.return_value = m_response
109 m_submit.return_value = m_response
110
110
111 params = {
111 params = {
112 'csrf_token': csrf_token,
112 'csrf_token': csrf_token,
113 'email': 'pytest@example.com',
113 'email': 'pytest@example.com',
114 'firstname': 'pytest-firstname',
114 'firstname': 'pytest-firstname',
115 'lastname': 'pytest-lastname',
115 'lastname': 'pytest-lastname',
116 'password': 'secret',
116 'password': 'secret',
117 'password_confirmation': 'secret',
117 'password_confirmation': 'secret',
118 'username': 'pytest',
118 'username': 'pytest',
119 }
119 }
120 response = app.post(ADMIN_PREFIX + '/register', params=params)
120 response = app.post(ADMIN_PREFIX + '/register', params=params)
121
121
122 if valid:
122 if valid:
123 # If we provided a valid captcha input we expect a successful
123 # If we provided a valid captcha input we expect a successful
124 # registration and redirect to the login page.
124 # registration and redirect to the login page.
125 assert response.status_int == 302
125 assert response.status_int == 302
126 assert 'location' in response.headers
126 assert 'location' in response.headers
127 assert ADMIN_PREFIX + '/login' in response.headers['location']
127 assert ADMIN_PREFIX + '/login' in response.headers['location']
128 else:
128 else:
129 # If captche input is invalid we expect to stay on the registration
129 # If captche input is invalid we expect to stay on the registration
130 # page with an error message displayed.
130 # page with an error message displayed.
131 assertr = AssertResponse(response)
131 assertr = AssertResponse(response)
132 assert response.status_int == 200
132 assert response.status_int == 200
133 assertr.one_element_exists('#recaptcha_field ~ span.error-message')
133 assertr.one_element_exists('#recaptcha_field ~ span.error-message')
@@ -1,425 +1,425 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import time
21 import time
22 import collections
22 import collections
23 import datetime
23 import datetime
24 import formencode
24 import formencode
25 import logging
25 import logging
26 import urlparse
26 import urlparse
27
27
28 from pyramid.httpexceptions import HTTPFound
28 from pyramid.httpexceptions import HTTPFound
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from recaptcha.client.captcha import submit
30 from recaptcha.client.captcha import submit
31
31
32 from rhodecode.apps._base import BaseAppView
32 from rhodecode.apps._base import BaseAppView
33 from rhodecode.authentication.base import authenticate, HTTP_TYPE
33 from rhodecode.authentication.base import authenticate, HTTP_TYPE
34 from rhodecode.events import UserRegistered
34 from rhodecode.events import UserRegistered
35 from rhodecode.lib import helpers as h
35 from rhodecode.lib import helpers as h
36 from rhodecode.lib import audit_logger
36 from rhodecode.lib import audit_logger
37 from rhodecode.lib.auth import (
37 from rhodecode.lib.auth import (
38 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
38 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
39 from rhodecode.lib.base import get_ip_addr
39 from rhodecode.lib.base import get_ip_addr
40 from rhodecode.lib.exceptions import UserCreationError
40 from rhodecode.lib.exceptions import UserCreationError
41 from rhodecode.lib.utils2 import safe_str
41 from rhodecode.lib.utils2 import safe_str
42 from rhodecode.model.db import User, UserApiKeys
42 from rhodecode.model.db import User, UserApiKeys
43 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
43 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
44 from rhodecode.model.meta import Session
44 from rhodecode.model.meta import Session
45 from rhodecode.model.auth_token import AuthTokenModel
45 from rhodecode.model.auth_token import AuthTokenModel
46 from rhodecode.model.settings import SettingsModel
46 from rhodecode.model.settings import SettingsModel
47 from rhodecode.model.user import UserModel
47 from rhodecode.model.user import UserModel
48 from rhodecode.translation import _
48 from rhodecode.translation import _
49
49
50
50
51 log = logging.getLogger(__name__)
51 log = logging.getLogger(__name__)
52
52
53 CaptchaData = collections.namedtuple(
53 CaptchaData = collections.namedtuple(
54 'CaptchaData', 'active, private_key, public_key')
54 'CaptchaData', 'active, private_key, public_key')
55
55
56
56
57 def _store_user_in_session(session, username, remember=False):
57 def _store_user_in_session(session, username, remember=False):
58 user = User.get_by_username(username, case_insensitive=True)
58 user = User.get_by_username(username, case_insensitive=True)
59 auth_user = AuthUser(user.user_id)
59 auth_user = AuthUser(user.user_id)
60 auth_user.set_authenticated()
60 auth_user.set_authenticated()
61 cs = auth_user.get_cookie_store()
61 cs = auth_user.get_cookie_store()
62 session['rhodecode_user'] = cs
62 session['rhodecode_user'] = cs
63 user.update_lastlogin()
63 user.update_lastlogin()
64 Session().commit()
64 Session().commit()
65
65
66 # If they want to be remembered, update the cookie
66 # If they want to be remembered, update the cookie
67 if remember:
67 if remember:
68 _year = (datetime.datetime.now() +
68 _year = (datetime.datetime.now() +
69 datetime.timedelta(seconds=60 * 60 * 24 * 365))
69 datetime.timedelta(seconds=60 * 60 * 24 * 365))
70 session._set_cookie_expires(_year)
70 session._set_cookie_expires(_year)
71
71
72 session.save()
72 session.save()
73
73
74 safe_cs = cs.copy()
74 safe_cs = cs.copy()
75 safe_cs['password'] = '****'
75 safe_cs['password'] = '****'
76 log.info('user %s is now authenticated and stored in '
76 log.info('user %s is now authenticated and stored in '
77 'session, session attrs %s', username, safe_cs)
77 'session, session attrs %s', username, safe_cs)
78
78
79 # dumps session attrs back to cookie
79 # dumps session attrs back to cookie
80 session._update_cookie_out()
80 session._update_cookie_out()
81 # we set new cookie
81 # we set new cookie
82 headers = None
82 headers = None
83 if session.request['set_cookie']:
83 if session.request['set_cookie']:
84 # send set-cookie headers back to response to update cookie
84 # send set-cookie headers back to response to update cookie
85 headers = [('Set-Cookie', session.request['cookie_out'])]
85 headers = [('Set-Cookie', session.request['cookie_out'])]
86 return headers
86 return headers
87
87
88
88
89 def get_came_from(request):
89 def get_came_from(request):
90 came_from = safe_str(request.GET.get('came_from', ''))
90 came_from = safe_str(request.GET.get('came_from', ''))
91 parsed = urlparse.urlparse(came_from)
91 parsed = urlparse.urlparse(came_from)
92 allowed_schemes = ['http', 'https']
92 allowed_schemes = ['http', 'https']
93 default_came_from = h.route_path('home')
93 default_came_from = h.route_path('home')
94 if parsed.scheme and parsed.scheme not in allowed_schemes:
94 if parsed.scheme and parsed.scheme not in allowed_schemes:
95 log.error('Suspicious URL scheme detected %s for url %s' %
95 log.error('Suspicious URL scheme detected %s for url %s' %
96 (parsed.scheme, parsed))
96 (parsed.scheme, parsed))
97 came_from = default_came_from
97 came_from = default_came_from
98 elif parsed.netloc and request.host != parsed.netloc:
98 elif parsed.netloc and request.host != parsed.netloc:
99 log.error('Suspicious NETLOC detected %s for url %s server url '
99 log.error('Suspicious NETLOC detected %s for url %s server url '
100 'is: %s' % (parsed.netloc, parsed, request.host))
100 'is: %s' % (parsed.netloc, parsed, request.host))
101 came_from = default_came_from
101 came_from = default_came_from
102 elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
102 elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
103 log.error('Header injection detected `%s` for url %s server url ' %
103 log.error('Header injection detected `%s` for url %s server url ' %
104 (parsed.path, parsed))
104 (parsed.path, parsed))
105 came_from = default_came_from
105 came_from = default_came_from
106
106
107 return came_from or default_came_from
107 return came_from or default_came_from
108
108
109
109
110 class LoginView(BaseAppView):
110 class LoginView(BaseAppView):
111
111
112 def load_default_context(self):
112 def load_default_context(self):
113 c = self._get_local_tmpl_context()
113 c = self._get_local_tmpl_context()
114 c.came_from = get_came_from(self.request)
114 c.came_from = get_came_from(self.request)
115 self._register_global_c(c)
115 self._register_global_c(c)
116 return c
116 return c
117
117
118 def _get_captcha_data(self):
118 def _get_captcha_data(self):
119 settings = SettingsModel().get_all_settings()
119 settings = SettingsModel().get_all_settings()
120 private_key = settings.get('rhodecode_captcha_private_key')
120 private_key = settings.get('rhodecode_captcha_private_key')
121 public_key = settings.get('rhodecode_captcha_public_key')
121 public_key = settings.get('rhodecode_captcha_public_key')
122 active = bool(private_key)
122 active = bool(private_key)
123 return CaptchaData(
123 return CaptchaData(
124 active=active, private_key=private_key, public_key=public_key)
124 active=active, private_key=private_key, public_key=public_key)
125
125
126 @view_config(
126 @view_config(
127 route_name='login', request_method='GET',
127 route_name='login', request_method='GET',
128 renderer='rhodecode:templates/login.mako')
128 renderer='rhodecode:templates/login.mako')
129 def login(self):
129 def login(self):
130 c = self.load_default_context()
130 c = self.load_default_context()
131 auth_user = self._rhodecode_user
131 auth_user = self._rhodecode_user
132
132
133 # redirect if already logged in
133 # redirect if already logged in
134 if (auth_user.is_authenticated and
134 if (auth_user.is_authenticated and
135 not auth_user.is_default and auth_user.ip_allowed):
135 not auth_user.is_default and auth_user.ip_allowed):
136 raise HTTPFound(c.came_from)
136 raise HTTPFound(c.came_from)
137
137
138 # check if we use headers plugin, and try to login using it.
138 # check if we use headers plugin, and try to login using it.
139 try:
139 try:
140 log.debug('Running PRE-AUTH for headers based authentication')
140 log.debug('Running PRE-AUTH for headers based authentication')
141 auth_info = authenticate(
141 auth_info = authenticate(
142 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
142 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
143 if auth_info:
143 if auth_info:
144 headers = _store_user_in_session(
144 headers = _store_user_in_session(
145 self.session, auth_info.get('username'))
145 self.session, auth_info.get('username'))
146 raise HTTPFound(c.came_from, headers=headers)
146 raise HTTPFound(c.came_from, headers=headers)
147 except UserCreationError as e:
147 except UserCreationError as e:
148 log.error(e)
148 log.error(e)
149 self.session.flash(e, queue='error')
149 self.session.flash(e, queue='error')
150
150
151 return self._get_template_context(c)
151 return self._get_template_context(c)
152
152
153 @view_config(
153 @view_config(
154 route_name='login', request_method='POST',
154 route_name='login', request_method='POST',
155 renderer='rhodecode:templates/login.mako')
155 renderer='rhodecode:templates/login.mako')
156 def login_post(self):
156 def login_post(self):
157 c = self.load_default_context()
157 c = self.load_default_context()
158
158
159 login_form = LoginForm()()
159 login_form = LoginForm()()
160
160
161 try:
161 try:
162 self.session.invalidate()
162 self.session.invalidate()
163 form_result = login_form.to_python(self.request.params)
163 form_result = login_form.to_python(self.request.params)
164 # form checks for username/password, now we're authenticated
164 # form checks for username/password, now we're authenticated
165 headers = _store_user_in_session(
165 headers = _store_user_in_session(
166 self.session,
166 self.session,
167 username=form_result['username'],
167 username=form_result['username'],
168 remember=form_result['remember'])
168 remember=form_result['remember'])
169 log.debug('Redirecting to "%s" after login.', c.came_from)
169 log.debug('Redirecting to "%s" after login.', c.came_from)
170
170
171 audit_user = audit_logger.UserWrap(
171 audit_user = audit_logger.UserWrap(
172 username=self.request.params.get('username'),
172 username=self.request.params.get('username'),
173 ip_addr=self.request.remote_addr)
173 ip_addr=self.request.remote_addr)
174 action_data = {'user_agent': self.request.user_agent}
174 action_data = {'user_agent': self.request.user_agent}
175 audit_logger.store_web(
175 audit_logger.store_web(
176 'user.login.success', action_data=action_data,
176 'user.login.success', action_data=action_data,
177 user=audit_user, commit=True)
177 user=audit_user, commit=True)
178
178
179 raise HTTPFound(c.came_from, headers=headers)
179 raise HTTPFound(c.came_from, headers=headers)
180 except formencode.Invalid as errors:
180 except formencode.Invalid as errors:
181 defaults = errors.value
181 defaults = errors.value
182 # remove password from filling in form again
182 # remove password from filling in form again
183 defaults.pop('password', None)
183 defaults.pop('password', None)
184 render_ctx = self._get_template_context(c)
184 render_ctx = self._get_template_context(c)
185 render_ctx.update({
185 render_ctx.update({
186 'errors': errors.error_dict,
186 'errors': errors.error_dict,
187 'defaults': defaults,
187 'defaults': defaults,
188 })
188 })
189
189
190 audit_user = audit_logger.UserWrap(
190 audit_user = audit_logger.UserWrap(
191 username=self.request.params.get('username'),
191 username=self.request.params.get('username'),
192 ip_addr=self.request.remote_addr)
192 ip_addr=self.request.remote_addr)
193 action_data = {'user_agent': self.request.user_agent}
193 action_data = {'user_agent': self.request.user_agent}
194 audit_logger.store_web(
194 audit_logger.store_web(
195 'user.login.failure', action_data=action_data,
195 'user.login.failure', action_data=action_data,
196 user=audit_user, commit=True)
196 user=audit_user, commit=True)
197 return render_ctx
197 return render_ctx
198
198
199 except UserCreationError as e:
199 except UserCreationError as e:
200 # headers auth or other auth functions that create users on
200 # headers auth or other auth functions that create users on
201 # the fly can throw this exception signaling that there's issue
201 # the fly can throw this exception signaling that there's issue
202 # with user creation, explanation should be provided in
202 # with user creation, explanation should be provided in
203 # Exception itself
203 # Exception itself
204 self.session.flash(e, queue='error')
204 self.session.flash(e, queue='error')
205 return self._get_template_context(c)
205 return self._get_template_context(c)
206
206
207 @CSRFRequired()
207 @CSRFRequired()
208 @view_config(route_name='logout', request_method='POST')
208 @view_config(route_name='logout', request_method='POST')
209 def logout(self):
209 def logout(self):
210 auth_user = self._rhodecode_user
210 auth_user = self._rhodecode_user
211 log.info('Deleting session for user: `%s`', auth_user)
211 log.info('Deleting session for user: `%s`', auth_user)
212
212
213 action_data = {'user_agent': self.request.user_agent}
213 action_data = {'user_agent': self.request.user_agent}
214 audit_logger.store_web(
214 audit_logger.store_web(
215 'user.logout', action_data=action_data,
215 'user.logout', action_data=action_data,
216 user=auth_user, commit=True)
216 user=auth_user, commit=True)
217 self.session.delete()
217 self.session.delete()
218 return HTTPFound(h.route_path('home'))
218 return HTTPFound(h.route_path('home'))
219
219
220 @HasPermissionAnyDecorator(
220 @HasPermissionAnyDecorator(
221 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
221 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
222 @view_config(
222 @view_config(
223 route_name='register', request_method='GET',
223 route_name='register', request_method='GET',
224 renderer='rhodecode:templates/register.mako',)
224 renderer='rhodecode:templates/register.mako',)
225 def register(self, defaults=None, errors=None):
225 def register(self, defaults=None, errors=None):
226 c = self.load_default_context()
226 c = self.load_default_context()
227 defaults = defaults or {}
227 defaults = defaults or {}
228 errors = errors or {}
228 errors = errors or {}
229
229
230 settings = SettingsModel().get_all_settings()
230 settings = SettingsModel().get_all_settings()
231 register_message = settings.get('rhodecode_register_message') or ''
231 register_message = settings.get('rhodecode_register_message') or ''
232 captcha = self._get_captcha_data()
232 captcha = self._get_captcha_data()
233 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
233 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
234 .AuthUser.permissions['global']
234 .AuthUser().permissions['global']
235
235
236 render_ctx = self._get_template_context(c)
236 render_ctx = self._get_template_context(c)
237 render_ctx.update({
237 render_ctx.update({
238 'defaults': defaults,
238 'defaults': defaults,
239 'errors': errors,
239 'errors': errors,
240 'auto_active': auto_active,
240 'auto_active': auto_active,
241 'captcha_active': captcha.active,
241 'captcha_active': captcha.active,
242 'captcha_public_key': captcha.public_key,
242 'captcha_public_key': captcha.public_key,
243 'register_message': register_message,
243 'register_message': register_message,
244 })
244 })
245 return render_ctx
245 return render_ctx
246
246
247 @HasPermissionAnyDecorator(
247 @HasPermissionAnyDecorator(
248 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
248 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
249 @view_config(
249 @view_config(
250 route_name='register', request_method='POST',
250 route_name='register', request_method='POST',
251 renderer='rhodecode:templates/register.mako')
251 renderer='rhodecode:templates/register.mako')
252 def register_post(self):
252 def register_post(self):
253 captcha = self._get_captcha_data()
253 captcha = self._get_captcha_data()
254 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
254 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
255 .AuthUser.permissions['global']
255 .AuthUser().permissions['global']
256
256
257 register_form = RegisterForm()()
257 register_form = RegisterForm()()
258 try:
258 try:
259 form_result = register_form.to_python(self.request.params)
259 form_result = register_form.to_python(self.request.params)
260 form_result['active'] = auto_active
260 form_result['active'] = auto_active
261
261
262 if captcha.active:
262 if captcha.active:
263 response = submit(
263 response = submit(
264 self.request.params.get('recaptcha_challenge_field'),
264 self.request.params.get('recaptcha_challenge_field'),
265 self.request.params.get('recaptcha_response_field'),
265 self.request.params.get('recaptcha_response_field'),
266 private_key=captcha.private_key,
266 private_key=captcha.private_key,
267 remoteip=get_ip_addr(self.request.environ))
267 remoteip=get_ip_addr(self.request.environ))
268 if not response.is_valid:
268 if not response.is_valid:
269 _value = form_result
269 _value = form_result
270 _msg = _('Bad captcha')
270 _msg = _('Bad captcha')
271 error_dict = {'recaptcha_field': _msg}
271 error_dict = {'recaptcha_field': _msg}
272 raise formencode.Invalid(_msg, _value, None,
272 raise formencode.Invalid(_msg, _value, None,
273 error_dict=error_dict)
273 error_dict=error_dict)
274
274
275 new_user = UserModel().create_registration(form_result)
275 new_user = UserModel().create_registration(form_result)
276 event = UserRegistered(user=new_user, session=self.session)
276 event = UserRegistered(user=new_user, session=self.session)
277 self.request.registry.notify(event)
277 self.request.registry.notify(event)
278 self.session.flash(
278 self.session.flash(
279 _('You have successfully registered with RhodeCode'),
279 _('You have successfully registered with RhodeCode'),
280 queue='success')
280 queue='success')
281 Session().commit()
281 Session().commit()
282
282
283 redirect_ro = self.request.route_path('login')
283 redirect_ro = self.request.route_path('login')
284 raise HTTPFound(redirect_ro)
284 raise HTTPFound(redirect_ro)
285
285
286 except formencode.Invalid as errors:
286 except formencode.Invalid as errors:
287 errors.value.pop('password', None)
287 errors.value.pop('password', None)
288 errors.value.pop('password_confirmation', None)
288 errors.value.pop('password_confirmation', None)
289 return self.register(
289 return self.register(
290 defaults=errors.value, errors=errors.error_dict)
290 defaults=errors.value, errors=errors.error_dict)
291
291
292 except UserCreationError as e:
292 except UserCreationError as e:
293 # container auth or other auth functions that create users on
293 # container auth or other auth functions that create users on
294 # the fly can throw this exception signaling that there's issue
294 # the fly can throw this exception signaling that there's issue
295 # with user creation, explanation should be provided in
295 # with user creation, explanation should be provided in
296 # Exception itself
296 # Exception itself
297 self.session.flash(e, queue='error')
297 self.session.flash(e, queue='error')
298 return self.register()
298 return self.register()
299
299
300 @view_config(
300 @view_config(
301 route_name='reset_password', request_method=('GET', 'POST'),
301 route_name='reset_password', request_method=('GET', 'POST'),
302 renderer='rhodecode:templates/password_reset.mako')
302 renderer='rhodecode:templates/password_reset.mako')
303 def password_reset(self):
303 def password_reset(self):
304 captcha = self._get_captcha_data()
304 captcha = self._get_captcha_data()
305
305
306 render_ctx = {
306 render_ctx = {
307 'captcha_active': captcha.active,
307 'captcha_active': captcha.active,
308 'captcha_public_key': captcha.public_key,
308 'captcha_public_key': captcha.public_key,
309 'defaults': {},
309 'defaults': {},
310 'errors': {},
310 'errors': {},
311 }
311 }
312
312
313 # always send implicit message to prevent from discovery of
313 # always send implicit message to prevent from discovery of
314 # matching emails
314 # matching emails
315 msg = _('If such email exists, a password reset link was sent to it.')
315 msg = _('If such email exists, a password reset link was sent to it.')
316
316
317 if self.request.POST:
317 if self.request.POST:
318 if h.HasPermissionAny('hg.password_reset.disabled')():
318 if h.HasPermissionAny('hg.password_reset.disabled')():
319 _email = self.request.POST.get('email', '')
319 _email = self.request.POST.get('email', '')
320 log.error('Failed attempt to reset password for `%s`.', _email)
320 log.error('Failed attempt to reset password for `%s`.', _email)
321 self.session.flash(_('Password reset has been disabled.'),
321 self.session.flash(_('Password reset has been disabled.'),
322 queue='error')
322 queue='error')
323 return HTTPFound(self.request.route_path('reset_password'))
323 return HTTPFound(self.request.route_path('reset_password'))
324
324
325 password_reset_form = PasswordResetForm()()
325 password_reset_form = PasswordResetForm()()
326 try:
326 try:
327 form_result = password_reset_form.to_python(
327 form_result = password_reset_form.to_python(
328 self.request.params)
328 self.request.params)
329 user_email = form_result['email']
329 user_email = form_result['email']
330
330
331 if captcha.active:
331 if captcha.active:
332 response = submit(
332 response = submit(
333 self.request.params.get('recaptcha_challenge_field'),
333 self.request.params.get('recaptcha_challenge_field'),
334 self.request.params.get('recaptcha_response_field'),
334 self.request.params.get('recaptcha_response_field'),
335 private_key=captcha.private_key,
335 private_key=captcha.private_key,
336 remoteip=get_ip_addr(self.request.environ))
336 remoteip=get_ip_addr(self.request.environ))
337 if not response.is_valid:
337 if not response.is_valid:
338 _value = form_result
338 _value = form_result
339 _msg = _('Bad captcha')
339 _msg = _('Bad captcha')
340 error_dict = {'recaptcha_field': _msg}
340 error_dict = {'recaptcha_field': _msg}
341 raise formencode.Invalid(
341 raise formencode.Invalid(
342 _msg, _value, None, error_dict=error_dict)
342 _msg, _value, None, error_dict=error_dict)
343
343
344 # Generate reset URL and send mail.
344 # Generate reset URL and send mail.
345 user = User.get_by_email(user_email)
345 user = User.get_by_email(user_email)
346
346
347 # generate password reset token that expires in 10minutes
347 # generate password reset token that expires in 10minutes
348 desc = 'Generated token for password reset from {}'.format(
348 desc = 'Generated token for password reset from {}'.format(
349 datetime.datetime.now().isoformat())
349 datetime.datetime.now().isoformat())
350 reset_token = AuthTokenModel().create(
350 reset_token = AuthTokenModel().create(
351 user, lifetime=10,
351 user, lifetime=10,
352 description=desc,
352 description=desc,
353 role=UserApiKeys.ROLE_PASSWORD_RESET)
353 role=UserApiKeys.ROLE_PASSWORD_RESET)
354 Session().commit()
354 Session().commit()
355
355
356 log.debug('Successfully created password recovery token')
356 log.debug('Successfully created password recovery token')
357 password_reset_url = self.request.route_url(
357 password_reset_url = self.request.route_url(
358 'reset_password_confirmation',
358 'reset_password_confirmation',
359 _query={'key': reset_token.api_key})
359 _query={'key': reset_token.api_key})
360 UserModel().reset_password_link(
360 UserModel().reset_password_link(
361 form_result, password_reset_url)
361 form_result, password_reset_url)
362 # Display success message and redirect.
362 # Display success message and redirect.
363 self.session.flash(msg, queue='success')
363 self.session.flash(msg, queue='success')
364
364
365 action_data = {'email': user_email,
365 action_data = {'email': user_email,
366 'user_agent': self.request.user_agent}
366 'user_agent': self.request.user_agent}
367 audit_logger.store_web(
367 audit_logger.store_web(
368 'user.password.reset_request', action_data=action_data,
368 'user.password.reset_request', action_data=action_data,
369 user=self._rhodecode_user, commit=True)
369 user=self._rhodecode_user, commit=True)
370 return HTTPFound(self.request.route_path('reset_password'))
370 return HTTPFound(self.request.route_path('reset_password'))
371
371
372 except formencode.Invalid as errors:
372 except formencode.Invalid as errors:
373 render_ctx.update({
373 render_ctx.update({
374 'defaults': errors.value,
374 'defaults': errors.value,
375 'errors': errors.error_dict,
375 'errors': errors.error_dict,
376 })
376 })
377 if not self.request.params.get('email'):
377 if not self.request.params.get('email'):
378 # case of empty email, we want to report that
378 # case of empty email, we want to report that
379 return render_ctx
379 return render_ctx
380
380
381 if 'recaptcha_field' in errors.error_dict:
381 if 'recaptcha_field' in errors.error_dict:
382 # case of failed captcha
382 # case of failed captcha
383 return render_ctx
383 return render_ctx
384
384
385 log.debug('faking response on invalid password reset')
385 log.debug('faking response on invalid password reset')
386 # make this take 2s, to prevent brute forcing.
386 # make this take 2s, to prevent brute forcing.
387 time.sleep(2)
387 time.sleep(2)
388 self.session.flash(msg, queue='success')
388 self.session.flash(msg, queue='success')
389 return HTTPFound(self.request.route_path('reset_password'))
389 return HTTPFound(self.request.route_path('reset_password'))
390
390
391 return render_ctx
391 return render_ctx
392
392
393 @view_config(route_name='reset_password_confirmation',
393 @view_config(route_name='reset_password_confirmation',
394 request_method='GET')
394 request_method='GET')
395 def password_reset_confirmation(self):
395 def password_reset_confirmation(self):
396
396
397 if self.request.GET and self.request.GET.get('key'):
397 if self.request.GET and self.request.GET.get('key'):
398 # make this take 2s, to prevent brute forcing.
398 # make this take 2s, to prevent brute forcing.
399 time.sleep(2)
399 time.sleep(2)
400
400
401 token = AuthTokenModel().get_auth_token(
401 token = AuthTokenModel().get_auth_token(
402 self.request.GET.get('key'))
402 self.request.GET.get('key'))
403
403
404 # verify token is the correct role
404 # verify token is the correct role
405 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
405 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
406 log.debug('Got token with role:%s expected is %s',
406 log.debug('Got token with role:%s expected is %s',
407 getattr(token, 'role', 'EMPTY_TOKEN'),
407 getattr(token, 'role', 'EMPTY_TOKEN'),
408 UserApiKeys.ROLE_PASSWORD_RESET)
408 UserApiKeys.ROLE_PASSWORD_RESET)
409 self.session.flash(
409 self.session.flash(
410 _('Given reset token is invalid'), queue='error')
410 _('Given reset token is invalid'), queue='error')
411 return HTTPFound(self.request.route_path('reset_password'))
411 return HTTPFound(self.request.route_path('reset_password'))
412
412
413 try:
413 try:
414 owner = token.user
414 owner = token.user
415 data = {'email': owner.email, 'token': token.api_key}
415 data = {'email': owner.email, 'token': token.api_key}
416 UserModel().reset_password(data)
416 UserModel().reset_password(data)
417 self.session.flash(
417 self.session.flash(
418 _('Your password reset was successful, '
418 _('Your password reset was successful, '
419 'a new password has been sent to your email'),
419 'a new password has been sent to your email'),
420 queue='success')
420 queue='success')
421 except Exception as e:
421 except Exception as e:
422 log.error(e)
422 log.error(e)
423 return HTTPFound(self.request.route_path('reset_password'))
423 return HTTPFound(self.request.route_path('reset_password'))
424
424
425 return HTTPFound(self.request.route_path('login'))
425 return HTTPFound(self.request.route_path('login'))
@@ -1,523 +1,523 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import re
21 import re
22
22
23 import mock
23 import mock
24 import pytest
24 import pytest
25
25
26 from rhodecode.apps.repository.views.repo_summary import RepoSummaryView
26 from rhodecode.apps.repository.views.repo_summary import RepoSummaryView
27 from rhodecode.lib import helpers as h
27 from rhodecode.lib import helpers as h
28 from rhodecode.lib.compat import OrderedDict
28 from rhodecode.lib.compat import OrderedDict
29 from rhodecode.lib.utils2 import AttributeDict, safe_str
29 from rhodecode.lib.utils2 import AttributeDict, safe_str
30 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
30 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
31 from rhodecode.model.db import Repository
31 from rhodecode.model.db import Repository
32 from rhodecode.model.meta import Session
32 from rhodecode.model.meta import Session
33 from rhodecode.model.repo import RepoModel
33 from rhodecode.model.repo import RepoModel
34 from rhodecode.model.scm import ScmModel
34 from rhodecode.model.scm import ScmModel
35 from rhodecode.tests import assert_session_flash
35 from rhodecode.tests import assert_session_flash
36 from rhodecode.tests.fixture import Fixture
36 from rhodecode.tests.fixture import Fixture
37 from rhodecode.tests.utils import AssertResponse, repo_on_filesystem
37 from rhodecode.tests.utils import AssertResponse, repo_on_filesystem
38
38
39
39
40 fixture = Fixture()
40 fixture = Fixture()
41
41
42
42
43 def route_path(name, params=None, **kwargs):
43 def route_path(name, params=None, **kwargs):
44 import urllib
44 import urllib
45
45
46 base_url = {
46 base_url = {
47 'repo_summary': '/{repo_name}',
47 'repo_summary': '/{repo_name}',
48 'repo_stats': '/{repo_name}/repo_stats/{commit_id}',
48 'repo_stats': '/{repo_name}/repo_stats/{commit_id}',
49 'repo_refs_data': '/{repo_name}/refs-data',
49 'repo_refs_data': '/{repo_name}/refs-data',
50 'repo_refs_changelog_data': '/{repo_name}/refs-data-changelog',
50 'repo_refs_changelog_data': '/{repo_name}/refs-data-changelog',
51 'repo_creating_check': '/{repo_name}/repo_creating_check',
51 'repo_creating_check': '/{repo_name}/repo_creating_check',
52 }[name].format(**kwargs)
52 }[name].format(**kwargs)
53
53
54 if params:
54 if params:
55 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
55 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
56 return base_url
56 return base_url
57
57
58
58
59 @pytest.mark.usefixtures('app')
59 @pytest.mark.usefixtures('app')
60 class TestSummaryView(object):
60 class TestSummaryView(object):
61 def test_index(self, autologin_user, backend, http_host_only_stub):
61 def test_index(self, autologin_user, backend, http_host_only_stub):
62 repo_id = backend.repo.repo_id
62 repo_id = backend.repo.repo_id
63 repo_name = backend.repo_name
63 repo_name = backend.repo_name
64 with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy',
64 with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy',
65 return_value=False):
65 return_value=False):
66 response = self.app.get(
66 response = self.app.get(
67 route_path('repo_summary', repo_name=repo_name))
67 route_path('repo_summary', repo_name=repo_name))
68
68
69 # repo type
69 # repo type
70 response.mustcontain(
70 response.mustcontain(
71 '<i class="icon-%s">' % (backend.alias, )
71 '<i class="icon-%s">' % (backend.alias, )
72 )
72 )
73 # public/private
73 # public/private
74 response.mustcontain(
74 response.mustcontain(
75 """<i class="icon-unlock-alt">"""
75 """<i class="icon-unlock-alt">"""
76 )
76 )
77
77
78 # clone url...
78 # clone url...
79 response.mustcontain(
79 response.mustcontain(
80 'id="clone_url" readonly="readonly"'
80 'id="clone_url" readonly="readonly"'
81 ' value="http://test_admin@%s/%s"' % (http_host_only_stub, repo_name, ))
81 ' value="http://test_admin@%s/%s"' % (http_host_only_stub, repo_name, ))
82 response.mustcontain(
82 response.mustcontain(
83 'id="clone_url_id" readonly="readonly"'
83 'id="clone_url_id" readonly="readonly"'
84 ' value="http://test_admin@%s/_%s"' % (http_host_only_stub, repo_id, ))
84 ' value="http://test_admin@%s/_%s"' % (http_host_only_stub, repo_id, ))
85
85
86 def test_index_svn_without_proxy(
86 def test_index_svn_without_proxy(
87 self, autologin_user, backend_svn, http_host_only_stub):
87 self, autologin_user, backend_svn, http_host_only_stub):
88 repo_id = backend_svn.repo.repo_id
88 repo_id = backend_svn.repo.repo_id
89 repo_name = backend_svn.repo_name
89 repo_name = backend_svn.repo_name
90 response = self.app.get(route_path('repo_summary', repo_name=repo_name))
90 response = self.app.get(route_path('repo_summary', repo_name=repo_name))
91 # clone url...
91 # clone url...
92 response.mustcontain(
92 response.mustcontain(
93 'id="clone_url" disabled'
93 'id="clone_url" disabled'
94 ' value="http://test_admin@%s/%s"' % (http_host_only_stub, repo_name, ))
94 ' value="http://test_admin@%s/%s"' % (http_host_only_stub, repo_name, ))
95 response.mustcontain(
95 response.mustcontain(
96 'id="clone_url_id" disabled'
96 'id="clone_url_id" disabled'
97 ' value="http://test_admin@%s/_%s"' % (http_host_only_stub, repo_id, ))
97 ' value="http://test_admin@%s/_%s"' % (http_host_only_stub, repo_id, ))
98
98
99 def test_index_with_trailing_slash(
99 def test_index_with_trailing_slash(
100 self, autologin_user, backend, http_host_only_stub):
100 self, autologin_user, backend, http_host_only_stub):
101
101
102 repo_id = backend.repo.repo_id
102 repo_id = backend.repo.repo_id
103 repo_name = backend.repo_name
103 repo_name = backend.repo_name
104 with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy',
104 with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy',
105 return_value=False):
105 return_value=False):
106 response = self.app.get(
106 response = self.app.get(
107 route_path('repo_summary', repo_name=repo_name) + '/',
107 route_path('repo_summary', repo_name=repo_name) + '/',
108 status=200)
108 status=200)
109
109
110 # clone url...
110 # clone url...
111 response.mustcontain(
111 response.mustcontain(
112 'id="clone_url" readonly="readonly"'
112 'id="clone_url" readonly="readonly"'
113 ' value="http://test_admin@%s/%s"' % (http_host_only_stub, repo_name, ))
113 ' value="http://test_admin@%s/%s"' % (http_host_only_stub, repo_name, ))
114 response.mustcontain(
114 response.mustcontain(
115 'id="clone_url_id" readonly="readonly"'
115 'id="clone_url_id" readonly="readonly"'
116 ' value="http://test_admin@%s/_%s"' % (http_host_only_stub, repo_id, ))
116 ' value="http://test_admin@%s/_%s"' % (http_host_only_stub, repo_id, ))
117
117
118 def test_index_by_id(self, autologin_user, backend):
118 def test_index_by_id(self, autologin_user, backend):
119 repo_id = backend.repo.repo_id
119 repo_id = backend.repo.repo_id
120 response = self.app.get(
120 response = self.app.get(
121 route_path('repo_summary', repo_name='_%s' % (repo_id,)))
121 route_path('repo_summary', repo_name='_%s' % (repo_id,)))
122
122
123 # repo type
123 # repo type
124 response.mustcontain(
124 response.mustcontain(
125 '<i class="icon-%s">' % (backend.alias, )
125 '<i class="icon-%s">' % (backend.alias, )
126 )
126 )
127 # public/private
127 # public/private
128 response.mustcontain(
128 response.mustcontain(
129 """<i class="icon-unlock-alt">"""
129 """<i class="icon-unlock-alt">"""
130 )
130 )
131
131
132 def test_index_by_repo_having_id_path_in_name_hg(self, autologin_user):
132 def test_index_by_repo_having_id_path_in_name_hg(self, autologin_user):
133 fixture.create_repo(name='repo_1')
133 fixture.create_repo(name='repo_1')
134 response = self.app.get(route_path('repo_summary', repo_name='repo_1'))
134 response = self.app.get(route_path('repo_summary', repo_name='repo_1'))
135
135
136 try:
136 try:
137 response.mustcontain("repo_1")
137 response.mustcontain("repo_1")
138 finally:
138 finally:
139 RepoModel().delete(Repository.get_by_repo_name('repo_1'))
139 RepoModel().delete(Repository.get_by_repo_name('repo_1'))
140 Session().commit()
140 Session().commit()
141
141
142 def test_index_with_anonymous_access_disabled(
142 def test_index_with_anonymous_access_disabled(
143 self, backend, disable_anonymous_user):
143 self, backend, disable_anonymous_user):
144 response = self.app.get(
144 response = self.app.get(
145 route_path('repo_summary', repo_name=backend.repo_name), status=302)
145 route_path('repo_summary', repo_name=backend.repo_name), status=302)
146 assert 'login' in response.location
146 assert 'login' in response.location
147
147
148 def _enable_stats(self, repo):
148 def _enable_stats(self, repo):
149 r = Repository.get_by_repo_name(repo)
149 r = Repository.get_by_repo_name(repo)
150 r.enable_statistics = True
150 r.enable_statistics = True
151 Session().add(r)
151 Session().add(r)
152 Session().commit()
152 Session().commit()
153
153
154 expected_trending = {
154 expected_trending = {
155 'hg': {
155 'hg': {
156 "py": {"count": 68, "desc": ["Python"]},
156 "py": {"count": 68, "desc": ["Python"]},
157 "rst": {"count": 16, "desc": ["Rst"]},
157 "rst": {"count": 16, "desc": ["Rst"]},
158 "css": {"count": 2, "desc": ["Css"]},
158 "css": {"count": 2, "desc": ["Css"]},
159 "sh": {"count": 2, "desc": ["Bash"]},
159 "sh": {"count": 2, "desc": ["Bash"]},
160 "bat": {"count": 1, "desc": ["Batch"]},
160 "bat": {"count": 1, "desc": ["Batch"]},
161 "cfg": {"count": 1, "desc": ["Ini"]},
161 "cfg": {"count": 1, "desc": ["Ini"]},
162 "html": {"count": 1, "desc": ["EvoqueHtml", "Html"]},
162 "html": {"count": 1, "desc": ["EvoqueHtml", "Html"]},
163 "ini": {"count": 1, "desc": ["Ini"]},
163 "ini": {"count": 1, "desc": ["Ini"]},
164 "js": {"count": 1, "desc": ["Javascript"]},
164 "js": {"count": 1, "desc": ["Javascript"]},
165 "makefile": {"count": 1, "desc": ["Makefile", "Makefile"]}
165 "makefile": {"count": 1, "desc": ["Makefile", "Makefile"]}
166 },
166 },
167 'git': {
167 'git': {
168 "py": {"count": 68, "desc": ["Python"]},
168 "py": {"count": 68, "desc": ["Python"]},
169 "rst": {"count": 16, "desc": ["Rst"]},
169 "rst": {"count": 16, "desc": ["Rst"]},
170 "css": {"count": 2, "desc": ["Css"]},
170 "css": {"count": 2, "desc": ["Css"]},
171 "sh": {"count": 2, "desc": ["Bash"]},
171 "sh": {"count": 2, "desc": ["Bash"]},
172 "bat": {"count": 1, "desc": ["Batch"]},
172 "bat": {"count": 1, "desc": ["Batch"]},
173 "cfg": {"count": 1, "desc": ["Ini"]},
173 "cfg": {"count": 1, "desc": ["Ini"]},
174 "html": {"count": 1, "desc": ["EvoqueHtml", "Html"]},
174 "html": {"count": 1, "desc": ["EvoqueHtml", "Html"]},
175 "ini": {"count": 1, "desc": ["Ini"]},
175 "ini": {"count": 1, "desc": ["Ini"]},
176 "js": {"count": 1, "desc": ["Javascript"]},
176 "js": {"count": 1, "desc": ["Javascript"]},
177 "makefile": {"count": 1, "desc": ["Makefile", "Makefile"]}
177 "makefile": {"count": 1, "desc": ["Makefile", "Makefile"]}
178 },
178 },
179 'svn': {
179 'svn': {
180 "py": {"count": 75, "desc": ["Python"]},
180 "py": {"count": 75, "desc": ["Python"]},
181 "rst": {"count": 16, "desc": ["Rst"]},
181 "rst": {"count": 16, "desc": ["Rst"]},
182 "html": {"count": 11, "desc": ["EvoqueHtml", "Html"]},
182 "html": {"count": 11, "desc": ["EvoqueHtml", "Html"]},
183 "css": {"count": 2, "desc": ["Css"]},
183 "css": {"count": 2, "desc": ["Css"]},
184 "bat": {"count": 1, "desc": ["Batch"]},
184 "bat": {"count": 1, "desc": ["Batch"]},
185 "cfg": {"count": 1, "desc": ["Ini"]},
185 "cfg": {"count": 1, "desc": ["Ini"]},
186 "ini": {"count": 1, "desc": ["Ini"]},
186 "ini": {"count": 1, "desc": ["Ini"]},
187 "js": {"count": 1, "desc": ["Javascript"]},
187 "js": {"count": 1, "desc": ["Javascript"]},
188 "makefile": {"count": 1, "desc": ["Makefile", "Makefile"]},
188 "makefile": {"count": 1, "desc": ["Makefile", "Makefile"]},
189 "sh": {"count": 1, "desc": ["Bash"]}
189 "sh": {"count": 1, "desc": ["Bash"]}
190 },
190 },
191 }
191 }
192
192
193 def test_repo_stats(self, autologin_user, backend, xhr_header):
193 def test_repo_stats(self, autologin_user, backend, xhr_header):
194 response = self.app.get(
194 response = self.app.get(
195 route_path(
195 route_path(
196 'repo_stats', repo_name=backend.repo_name, commit_id='tip'),
196 'repo_stats', repo_name=backend.repo_name, commit_id='tip'),
197 extra_environ=xhr_header,
197 extra_environ=xhr_header,
198 status=200)
198 status=200)
199 assert re.match(r'6[\d\.]+ KiB', response.json['size'])
199 assert re.match(r'6[\d\.]+ KiB', response.json['size'])
200
200
201 def test_repo_stats_code_stats_enabled(self, autologin_user, backend, xhr_header):
201 def test_repo_stats_code_stats_enabled(self, autologin_user, backend, xhr_header):
202 repo_name = backend.repo_name
202 repo_name = backend.repo_name
203
203
204 # codes stats
204 # codes stats
205 self._enable_stats(repo_name)
205 self._enable_stats(repo_name)
206 ScmModel().mark_for_invalidation(repo_name)
206 ScmModel().mark_for_invalidation(repo_name)
207
207
208 response = self.app.get(
208 response = self.app.get(
209 route_path(
209 route_path(
210 'repo_stats', repo_name=backend.repo_name, commit_id='tip'),
210 'repo_stats', repo_name=backend.repo_name, commit_id='tip'),
211 extra_environ=xhr_header,
211 extra_environ=xhr_header,
212 status=200)
212 status=200)
213
213
214 expected_data = self.expected_trending[backend.alias]
214 expected_data = self.expected_trending[backend.alias]
215 returned_stats = response.json['code_stats']
215 returned_stats = response.json['code_stats']
216 for k, v in expected_data.items():
216 for k, v in expected_data.items():
217 assert v == returned_stats[k]
217 assert v == returned_stats[k]
218
218
219 def test_repo_refs_data(self, backend):
219 def test_repo_refs_data(self, backend):
220 response = self.app.get(
220 response = self.app.get(
221 route_path('repo_refs_data', repo_name=backend.repo_name),
221 route_path('repo_refs_data', repo_name=backend.repo_name),
222 status=200)
222 status=200)
223
223
224 # Ensure that there is the correct amount of items in the result
224 # Ensure that there is the correct amount of items in the result
225 repo = backend.repo.scm_instance()
225 repo = backend.repo.scm_instance()
226 data = response.json['results']
226 data = response.json['results']
227 items = sum(len(section['children']) for section in data)
227 items = sum(len(section['children']) for section in data)
228 repo_refs = len(repo.branches) + len(repo.tags) + len(repo.bookmarks)
228 repo_refs = len(repo.branches) + len(repo.tags) + len(repo.bookmarks)
229 assert items == repo_refs
229 assert items == repo_refs
230
230
231 def test_index_shows_missing_requirements_message(
231 def test_index_shows_missing_requirements_message(
232 self, backend, autologin_user):
232 self, backend, autologin_user):
233 repo_name = backend.repo_name
233 repo_name = backend.repo_name
234 scm_patcher = mock.patch.object(
234 scm_patcher = mock.patch.object(
235 Repository, 'scm_instance', side_effect=RepositoryRequirementError)
235 Repository, 'scm_instance', side_effect=RepositoryRequirementError)
236
236
237 with scm_patcher:
237 with scm_patcher:
238 response = self.app.get(route_path('repo_summary', repo_name=repo_name))
238 response = self.app.get(route_path('repo_summary', repo_name=repo_name))
239 assert_response = AssertResponse(response)
239 assert_response = AssertResponse(response)
240 assert_response.element_contains(
240 assert_response.element_contains(
241 '.main .alert-warning strong', 'Missing requirements')
241 '.main .alert-warning strong', 'Missing requirements')
242 assert_response.element_contains(
242 assert_response.element_contains(
243 '.main .alert-warning',
243 '.main .alert-warning',
244 'Commits cannot be displayed, because this repository '
244 'Commits cannot be displayed, because this repository '
245 'uses one or more extensions, which was not enabled.')
245 'uses one or more extensions, which was not enabled.')
246
246
247 def test_missing_requirements_page_does_not_contains_switch_to(
247 def test_missing_requirements_page_does_not_contains_switch_to(
248 self, autologin_user, backend):
248 self, autologin_user, backend):
249 repo_name = backend.repo_name
249 repo_name = backend.repo_name
250 scm_patcher = mock.patch.object(
250 scm_patcher = mock.patch.object(
251 Repository, 'scm_instance', side_effect=RepositoryRequirementError)
251 Repository, 'scm_instance', side_effect=RepositoryRequirementError)
252
252
253 with scm_patcher:
253 with scm_patcher:
254 response = self.app.get(route_path('repo_summary', repo_name=repo_name))
254 response = self.app.get(route_path('repo_summary', repo_name=repo_name))
255 response.mustcontain(no='Switch To')
255 response.mustcontain(no='Switch To')
256
256
257
257
258 @pytest.mark.usefixtures('app')
258 @pytest.mark.usefixtures('app')
259 class TestRepoLocation(object):
259 class TestRepoLocation(object):
260
260
261 @pytest.mark.parametrize("suffix", [u'', u'Δ…Δ™Ε‚'], ids=['', 'non-ascii'])
261 @pytest.mark.parametrize("suffix", [u'', u'Δ…Δ™Ε‚'], ids=['', 'non-ascii'])
262 def test_missing_filesystem_repo(
262 def test_missing_filesystem_repo(
263 self, autologin_user, backend, suffix, csrf_token):
263 self, autologin_user, backend, suffix, csrf_token):
264 repo = backend.create_repo(name_suffix=suffix)
264 repo = backend.create_repo(name_suffix=suffix)
265 repo_name = repo.repo_name
265 repo_name = repo.repo_name
266
266
267 # delete from file system
267 # delete from file system
268 RepoModel()._delete_filesystem_repo(repo)
268 RepoModel()._delete_filesystem_repo(repo)
269
269
270 # test if the repo is still in the database
270 # test if the repo is still in the database
271 new_repo = RepoModel().get_by_repo_name(repo_name)
271 new_repo = RepoModel().get_by_repo_name(repo_name)
272 assert new_repo.repo_name == repo_name
272 assert new_repo.repo_name == repo_name
273
273
274 # check if repo is not in the filesystem
274 # check if repo is not in the filesystem
275 assert not repo_on_filesystem(repo_name)
275 assert not repo_on_filesystem(repo_name)
276
276
277 response = self.app.get(
277 response = self.app.get(
278 route_path('repo_summary', repo_name=safe_str(repo_name)), status=302)
278 route_path('repo_summary', repo_name=safe_str(repo_name)), status=302)
279
279
280 msg = 'The repository `%s` cannot be loaded in filesystem. ' \
280 msg = 'The repository `%s` cannot be loaded in filesystem. ' \
281 'Please check if it exist, or is not damaged.' % repo_name
281 'Please check if it exist, or is not damaged.' % repo_name
282 assert_session_flash(response, msg)
282 assert_session_flash(response, msg)
283
283
284 @pytest.mark.parametrize("suffix", [u'', u'Δ…Δ™Ε‚'], ids=['', 'non-ascii'])
284 @pytest.mark.parametrize("suffix", [u'', u'Δ…Δ™Ε‚'], ids=['', 'non-ascii'])
285 def test_missing_filesystem_repo_on_repo_check(
285 def test_missing_filesystem_repo_on_repo_check(
286 self, autologin_user, backend, suffix, csrf_token):
286 self, autologin_user, backend, suffix, csrf_token):
287 repo = backend.create_repo(name_suffix=suffix)
287 repo = backend.create_repo(name_suffix=suffix)
288 repo_name = repo.repo_name
288 repo_name = repo.repo_name
289
289
290 # delete from file system
290 # delete from file system
291 RepoModel()._delete_filesystem_repo(repo)
291 RepoModel()._delete_filesystem_repo(repo)
292
292
293 # test if the repo is still in the database
293 # test if the repo is still in the database
294 new_repo = RepoModel().get_by_repo_name(repo_name)
294 new_repo = RepoModel().get_by_repo_name(repo_name)
295 assert new_repo.repo_name == repo_name
295 assert new_repo.repo_name == repo_name
296
296
297 # check if repo is not in the filesystem
297 # check if repo is not in the filesystem
298 assert not repo_on_filesystem(repo_name)
298 assert not repo_on_filesystem(repo_name)
299
299
300 # flush the session
300 # flush the session
301 self.app.get(
301 self.app.get(
302 route_path('repo_summary', repo_name=safe_str(repo_name)),
302 route_path('repo_summary', repo_name=safe_str(repo_name)),
303 status=302)
303 status=302)
304
304
305 response = self.app.get(
305 response = self.app.get(
306 route_path('repo_creating_check', repo_name=safe_str(repo_name)),
306 route_path('repo_creating_check', repo_name=safe_str(repo_name)),
307 status=200)
307 status=200)
308 msg = 'The repository `%s` cannot be loaded in filesystem. ' \
308 msg = 'The repository `%s` cannot be loaded in filesystem. ' \
309 'Please check if it exist, or is not damaged.' % repo_name
309 'Please check if it exist, or is not damaged.' % repo_name
310 assert_session_flash(response, msg )
310 assert_session_flash(response, msg )
311
311
312
312
313 @pytest.fixture()
313 @pytest.fixture()
314 def summary_view(context_stub, request_stub, user_util):
314 def summary_view(context_stub, request_stub, user_util):
315 """
315 """
316 Bootstrap view to test the view functions
316 Bootstrap view to test the view functions
317 """
317 """
318 request_stub.matched_route = AttributeDict(name='test_view')
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 request_stub.db_repo = user_util.create_repo()
321 request_stub.db_repo = user_util.create_repo()
322
322
323 view = RepoSummaryView(context=context_stub, request=request_stub)
323 view = RepoSummaryView(context=context_stub, request=request_stub)
324 return view
324 return view
325
325
326
326
327 @pytest.mark.usefixtures('app')
327 @pytest.mark.usefixtures('app')
328 class TestCreateReferenceData(object):
328 class TestCreateReferenceData(object):
329
329
330 @pytest.fixture
330 @pytest.fixture
331 def example_refs(self):
331 def example_refs(self):
332 section_1_refs = OrderedDict((('a', 'a_id'), ('b', 'b_id')))
332 section_1_refs = OrderedDict((('a', 'a_id'), ('b', 'b_id')))
333 example_refs = [
333 example_refs = [
334 ('section_1', section_1_refs, 't1'),
334 ('section_1', section_1_refs, 't1'),
335 ('section_2', {'c': 'c_id'}, 't2'),
335 ('section_2', {'c': 'c_id'}, 't2'),
336 ]
336 ]
337 return example_refs
337 return example_refs
338
338
339 def test_generates_refs_based_on_commit_ids(self, example_refs, summary_view):
339 def test_generates_refs_based_on_commit_ids(self, example_refs, summary_view):
340 repo = mock.Mock()
340 repo = mock.Mock()
341 repo.name = 'test-repo'
341 repo.name = 'test-repo'
342 repo.alias = 'git'
342 repo.alias = 'git'
343 full_repo_name = 'pytest-repo-group/' + repo.name
343 full_repo_name = 'pytest-repo-group/' + repo.name
344
344
345 result = summary_view._create_reference_data(
345 result = summary_view._create_reference_data(
346 repo, full_repo_name, example_refs)
346 repo, full_repo_name, example_refs)
347
347
348 expected_files_url = '/{}/files/'.format(full_repo_name)
348 expected_files_url = '/{}/files/'.format(full_repo_name)
349 expected_result = [
349 expected_result = [
350 {
350 {
351 'children': [
351 'children': [
352 {
352 {
353 'id': 'a', 'raw_id': 'a_id', 'text': 'a', 'type': 't1',
353 'id': 'a', 'raw_id': 'a_id', 'text': 'a', 'type': 't1',
354 'files_url': expected_files_url + 'a/?at=a',
354 'files_url': expected_files_url + 'a/?at=a',
355 },
355 },
356 {
356 {
357 'id': 'b', 'raw_id': 'b_id', 'text': 'b', 'type': 't1',
357 'id': 'b', 'raw_id': 'b_id', 'text': 'b', 'type': 't1',
358 'files_url': expected_files_url + 'b/?at=b',
358 'files_url': expected_files_url + 'b/?at=b',
359 }
359 }
360 ],
360 ],
361 'text': 'section_1'
361 'text': 'section_1'
362 },
362 },
363 {
363 {
364 'children': [
364 'children': [
365 {
365 {
366 'id': 'c', 'raw_id': 'c_id', 'text': 'c', 'type': 't2',
366 'id': 'c', 'raw_id': 'c_id', 'text': 'c', 'type': 't2',
367 'files_url': expected_files_url + 'c/?at=c',
367 'files_url': expected_files_url + 'c/?at=c',
368 }
368 }
369 ],
369 ],
370 'text': 'section_2'
370 'text': 'section_2'
371 }]
371 }]
372 assert result == expected_result
372 assert result == expected_result
373
373
374 def test_generates_refs_with_path_for_svn(self, example_refs, summary_view):
374 def test_generates_refs_with_path_for_svn(self, example_refs, summary_view):
375 repo = mock.Mock()
375 repo = mock.Mock()
376 repo.name = 'test-repo'
376 repo.name = 'test-repo'
377 repo.alias = 'svn'
377 repo.alias = 'svn'
378 full_repo_name = 'pytest-repo-group/' + repo.name
378 full_repo_name = 'pytest-repo-group/' + repo.name
379
379
380 result = summary_view._create_reference_data(
380 result = summary_view._create_reference_data(
381 repo, full_repo_name, example_refs)
381 repo, full_repo_name, example_refs)
382
382
383 expected_files_url = '/{}/files/'.format(full_repo_name)
383 expected_files_url = '/{}/files/'.format(full_repo_name)
384 expected_result = [
384 expected_result = [
385 {
385 {
386 'children': [
386 'children': [
387 {
387 {
388 'id': 'a@a_id', 'raw_id': 'a_id',
388 'id': 'a@a_id', 'raw_id': 'a_id',
389 'text': 'a', 'type': 't1',
389 'text': 'a', 'type': 't1',
390 'files_url': expected_files_url + 'a_id/a?at=a',
390 'files_url': expected_files_url + 'a_id/a?at=a',
391 },
391 },
392 {
392 {
393 'id': 'b@b_id', 'raw_id': 'b_id',
393 'id': 'b@b_id', 'raw_id': 'b_id',
394 'text': 'b', 'type': 't1',
394 'text': 'b', 'type': 't1',
395 'files_url': expected_files_url + 'b_id/b?at=b',
395 'files_url': expected_files_url + 'b_id/b?at=b',
396 }
396 }
397 ],
397 ],
398 'text': 'section_1'
398 'text': 'section_1'
399 },
399 },
400 {
400 {
401 'children': [
401 'children': [
402 {
402 {
403 'id': 'c@c_id', 'raw_id': 'c_id',
403 'id': 'c@c_id', 'raw_id': 'c_id',
404 'text': 'c', 'type': 't2',
404 'text': 'c', 'type': 't2',
405 'files_url': expected_files_url + 'c_id/c?at=c',
405 'files_url': expected_files_url + 'c_id/c?at=c',
406 }
406 }
407 ],
407 ],
408 'text': 'section_2'
408 'text': 'section_2'
409 }
409 }
410 ]
410 ]
411 assert result == expected_result
411 assert result == expected_result
412
412
413
413
414 class TestCreateFilesUrl(object):
414 class TestCreateFilesUrl(object):
415
415
416 def test_creates_non_svn_url(self, app, summary_view):
416 def test_creates_non_svn_url(self, app, summary_view):
417 repo = mock.Mock()
417 repo = mock.Mock()
418 repo.name = 'abcde'
418 repo.name = 'abcde'
419 full_repo_name = 'test-repo-group/' + repo.name
419 full_repo_name = 'test-repo-group/' + repo.name
420 ref_name = 'branch1'
420 ref_name = 'branch1'
421 raw_id = 'deadbeef0123456789'
421 raw_id = 'deadbeef0123456789'
422 is_svn = False
422 is_svn = False
423
423
424 with mock.patch('rhodecode.lib.helpers.route_path') as url_mock:
424 with mock.patch('rhodecode.lib.helpers.route_path') as url_mock:
425 result = summary_view._create_files_url(
425 result = summary_view._create_files_url(
426 repo, full_repo_name, ref_name, raw_id, is_svn)
426 repo, full_repo_name, ref_name, raw_id, is_svn)
427 url_mock.assert_called_once_with(
427 url_mock.assert_called_once_with(
428 'repo_files', repo_name=full_repo_name, commit_id=ref_name,
428 'repo_files', repo_name=full_repo_name, commit_id=ref_name,
429 f_path='', _query=dict(at=ref_name))
429 f_path='', _query=dict(at=ref_name))
430 assert result == url_mock.return_value
430 assert result == url_mock.return_value
431
431
432 def test_creates_svn_url(self, app, summary_view):
432 def test_creates_svn_url(self, app, summary_view):
433 repo = mock.Mock()
433 repo = mock.Mock()
434 repo.name = 'abcde'
434 repo.name = 'abcde'
435 full_repo_name = 'test-repo-group/' + repo.name
435 full_repo_name = 'test-repo-group/' + repo.name
436 ref_name = 'branch1'
436 ref_name = 'branch1'
437 raw_id = 'deadbeef0123456789'
437 raw_id = 'deadbeef0123456789'
438 is_svn = True
438 is_svn = True
439
439
440 with mock.patch('rhodecode.lib.helpers.route_path') as url_mock:
440 with mock.patch('rhodecode.lib.helpers.route_path') as url_mock:
441 result = summary_view._create_files_url(
441 result = summary_view._create_files_url(
442 repo, full_repo_name, ref_name, raw_id, is_svn)
442 repo, full_repo_name, ref_name, raw_id, is_svn)
443 url_mock.assert_called_once_with(
443 url_mock.assert_called_once_with(
444 'repo_files', repo_name=full_repo_name, f_path=ref_name,
444 'repo_files', repo_name=full_repo_name, f_path=ref_name,
445 commit_id=raw_id, _query=dict(at=ref_name))
445 commit_id=raw_id, _query=dict(at=ref_name))
446 assert result == url_mock.return_value
446 assert result == url_mock.return_value
447
447
448 def test_name_has_slashes(self, app, summary_view):
448 def test_name_has_slashes(self, app, summary_view):
449 repo = mock.Mock()
449 repo = mock.Mock()
450 repo.name = 'abcde'
450 repo.name = 'abcde'
451 full_repo_name = 'test-repo-group/' + repo.name
451 full_repo_name = 'test-repo-group/' + repo.name
452 ref_name = 'branch1/branch2'
452 ref_name = 'branch1/branch2'
453 raw_id = 'deadbeef0123456789'
453 raw_id = 'deadbeef0123456789'
454 is_svn = False
454 is_svn = False
455
455
456 with mock.patch('rhodecode.lib.helpers.route_path') as url_mock:
456 with mock.patch('rhodecode.lib.helpers.route_path') as url_mock:
457 result = summary_view._create_files_url(
457 result = summary_view._create_files_url(
458 repo, full_repo_name, ref_name, raw_id, is_svn)
458 repo, full_repo_name, ref_name, raw_id, is_svn)
459 url_mock.assert_called_once_with(
459 url_mock.assert_called_once_with(
460 'repo_files', repo_name=full_repo_name, commit_id=raw_id,
460 'repo_files', repo_name=full_repo_name, commit_id=raw_id,
461 f_path='', _query=dict(at=ref_name))
461 f_path='', _query=dict(at=ref_name))
462 assert result == url_mock.return_value
462 assert result == url_mock.return_value
463
463
464
464
465 class TestReferenceItems(object):
465 class TestReferenceItems(object):
466 repo = mock.Mock()
466 repo = mock.Mock()
467 repo.name = 'pytest-repo'
467 repo.name = 'pytest-repo'
468 repo_full_name = 'pytest-repo-group/' + repo.name
468 repo_full_name = 'pytest-repo-group/' + repo.name
469 ref_type = 'branch'
469 ref_type = 'branch'
470 fake_url = '/abcde/'
470 fake_url = '/abcde/'
471
471
472 @staticmethod
472 @staticmethod
473 def _format_function(name, id_):
473 def _format_function(name, id_):
474 return 'format_function_{}_{}'.format(name, id_)
474 return 'format_function_{}_{}'.format(name, id_)
475
475
476 def test_creates_required_amount_of_items(self, summary_view):
476 def test_creates_required_amount_of_items(self, summary_view):
477 amount = 100
477 amount = 100
478 refs = {
478 refs = {
479 'ref{}'.format(i): '{0:040d}'.format(i)
479 'ref{}'.format(i): '{0:040d}'.format(i)
480 for i in range(amount)
480 for i in range(amount)
481 }
481 }
482
482
483 url_patcher = mock.patch.object(summary_view, '_create_files_url')
483 url_patcher = mock.patch.object(summary_view, '_create_files_url')
484 svn_patcher = mock.patch('rhodecode.lib.helpers.is_svn',
484 svn_patcher = mock.patch('rhodecode.lib.helpers.is_svn',
485 return_value=False)
485 return_value=False)
486
486
487 with url_patcher as url_mock, svn_patcher:
487 with url_patcher as url_mock, svn_patcher:
488 result = summary_view._create_reference_items(
488 result = summary_view._create_reference_items(
489 self.repo, self.repo_full_name, refs, self.ref_type,
489 self.repo, self.repo_full_name, refs, self.ref_type,
490 self._format_function)
490 self._format_function)
491 assert len(result) == amount
491 assert len(result) == amount
492 assert url_mock.call_count == amount
492 assert url_mock.call_count == amount
493
493
494 def test_single_item_details(self, summary_view):
494 def test_single_item_details(self, summary_view):
495 ref_name = 'ref1'
495 ref_name = 'ref1'
496 ref_id = 'deadbeef'
496 ref_id = 'deadbeef'
497 refs = {
497 refs = {
498 ref_name: ref_id
498 ref_name: ref_id
499 }
499 }
500
500
501 svn_patcher = mock.patch('rhodecode.lib.helpers.is_svn',
501 svn_patcher = mock.patch('rhodecode.lib.helpers.is_svn',
502 return_value=False)
502 return_value=False)
503
503
504 url_patcher = mock.patch.object(
504 url_patcher = mock.patch.object(
505 summary_view, '_create_files_url', return_value=self.fake_url)
505 summary_view, '_create_files_url', return_value=self.fake_url)
506
506
507 with url_patcher as url_mock, svn_patcher:
507 with url_patcher as url_mock, svn_patcher:
508 result = summary_view._create_reference_items(
508 result = summary_view._create_reference_items(
509 self.repo, self.repo_full_name, refs, self.ref_type,
509 self.repo, self.repo_full_name, refs, self.ref_type,
510 self._format_function)
510 self._format_function)
511
511
512 url_mock.assert_called_once_with(
512 url_mock.assert_called_once_with(
513 self.repo, self.repo_full_name, ref_name, ref_id, False)
513 self.repo, self.repo_full_name, ref_name, ref_id, False)
514 expected_result = [
514 expected_result = [
515 {
515 {
516 'text': ref_name,
516 'text': ref_name,
517 'id': self._format_function(ref_name, ref_id),
517 'id': self._format_function(ref_name, ref_id),
518 'raw_id': ref_id,
518 'raw_id': ref_id,
519 'type': self.ref_type,
519 'type': self.ref_type,
520 'files_url': self.fake_url
520 'files_url': self.fake_url
521 }
521 }
522 ]
522 ]
523 assert result == expected_result
523 assert result == expected_result
@@ -1,284 +1,284 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 RhodeCode authentication plugin for Atlassian CROWD
22 RhodeCode authentication plugin for Atlassian CROWD
23 """
23 """
24
24
25
25
26 import colander
26 import colander
27 import base64
27 import base64
28 import logging
28 import logging
29 import urllib2
29 import urllib2
30
30
31 from rhodecode.translation import _
31 from rhodecode.translation import _
32 from rhodecode.authentication.base import (
32 from rhodecode.authentication.base import (
33 RhodeCodeExternalAuthPlugin, hybrid_property)
33 RhodeCodeExternalAuthPlugin, hybrid_property)
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
36 from rhodecode.lib.colander_utils import strip_whitespace
36 from rhodecode.lib.colander_utils import strip_whitespace
37 from rhodecode.lib.ext_json import json, formatted_json
37 from rhodecode.lib.ext_json import json, formatted_json
38 from rhodecode.model.db import User
38 from rhodecode.model.db import User
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 def plugin_factory(plugin_id, *args, **kwds):
43 def plugin_factory(plugin_id, *args, **kwds):
44 """
44 """
45 Factory function that is called during plugin discovery.
45 Factory function that is called during plugin discovery.
46 It returns the plugin instance.
46 It returns the plugin instance.
47 """
47 """
48 plugin = RhodeCodeAuthPlugin(plugin_id)
48 plugin = RhodeCodeAuthPlugin(plugin_id)
49 return plugin
49 return plugin
50
50
51
51
52 class CrowdAuthnResource(AuthnPluginResourceBase):
52 class CrowdAuthnResource(AuthnPluginResourceBase):
53 pass
53 pass
54
54
55
55
56 class CrowdSettingsSchema(AuthnPluginSettingsSchemaBase):
56 class CrowdSettingsSchema(AuthnPluginSettingsSchemaBase):
57 host = colander.SchemaNode(
57 host = colander.SchemaNode(
58 colander.String(),
58 colander.String(),
59 default='127.0.0.1',
59 default='127.0.0.1',
60 description=_('The FQDN or IP of the Atlassian CROWD Server'),
60 description=_('The FQDN or IP of the Atlassian CROWD Server'),
61 preparer=strip_whitespace,
61 preparer=strip_whitespace,
62 title=_('Host'),
62 title=_('Host'),
63 widget='string')
63 widget='string')
64 port = colander.SchemaNode(
64 port = colander.SchemaNode(
65 colander.Int(),
65 colander.Int(),
66 default=8095,
66 default=8095,
67 description=_('The Port in use by the Atlassian CROWD Server'),
67 description=_('The Port in use by the Atlassian CROWD Server'),
68 preparer=strip_whitespace,
68 preparer=strip_whitespace,
69 title=_('Port'),
69 title=_('Port'),
70 validator=colander.Range(min=0, max=65536),
70 validator=colander.Range(min=0, max=65536),
71 widget='int')
71 widget='int')
72 app_name = colander.SchemaNode(
72 app_name = colander.SchemaNode(
73 colander.String(),
73 colander.String(),
74 default='',
74 default='',
75 description=_('The Application Name to authenticate to CROWD'),
75 description=_('The Application Name to authenticate to CROWD'),
76 preparer=strip_whitespace,
76 preparer=strip_whitespace,
77 title=_('Application Name'),
77 title=_('Application Name'),
78 widget='string')
78 widget='string')
79 app_password = colander.SchemaNode(
79 app_password = colander.SchemaNode(
80 colander.String(),
80 colander.String(),
81 default='',
81 default='',
82 description=_('The password to authenticate to CROWD'),
82 description=_('The password to authenticate to CROWD'),
83 preparer=strip_whitespace,
83 preparer=strip_whitespace,
84 title=_('Application Password'),
84 title=_('Application Password'),
85 widget='password')
85 widget='password')
86 admin_groups = colander.SchemaNode(
86 admin_groups = colander.SchemaNode(
87 colander.String(),
87 colander.String(),
88 default='',
88 default='',
89 description=_('A comma separated list of group names that identify '
89 description=_('A comma separated list of group names that identify '
90 'users as RhodeCode Administrators'),
90 'users as RhodeCode Administrators'),
91 missing='',
91 missing='',
92 preparer=strip_whitespace,
92 preparer=strip_whitespace,
93 title=_('Admin Groups'),
93 title=_('Admin Groups'),
94 widget='string')
94 widget='string')
95
95
96
96
97 class CrowdServer(object):
97 class CrowdServer(object):
98 def __init__(self, *args, **kwargs):
98 def __init__(self, *args, **kwargs):
99 """
99 """
100 Create a new CrowdServer object that points to IP/Address 'host',
100 Create a new CrowdServer object that points to IP/Address 'host',
101 on the given port, and using the given method (https/http). user and
101 on the given port, and using the given method (https/http). user and
102 passwd can be set here or with set_credentials. If unspecified,
102 passwd can be set here or with set_credentials. If unspecified,
103 "version" defaults to "latest".
103 "version" defaults to "latest".
104
104
105 example::
105 example::
106
106
107 cserver = CrowdServer(host="127.0.0.1",
107 cserver = CrowdServer(host="127.0.0.1",
108 port="8095",
108 port="8095",
109 user="some_app",
109 user="some_app",
110 passwd="some_passwd",
110 passwd="some_passwd",
111 version="1")
111 version="1")
112 """
112 """
113 if not "port" in kwargs:
113 if not "port" in kwargs:
114 kwargs["port"] = "8095"
114 kwargs["port"] = "8095"
115 self._logger = kwargs.get("logger", logging.getLogger(__name__))
115 self._logger = kwargs.get("logger", logging.getLogger(__name__))
116 self._uri = "%s://%s:%s/crowd" % (kwargs.get("method", "http"),
116 self._uri = "%s://%s:%s/crowd" % (kwargs.get("method", "http"),
117 kwargs.get("host", "127.0.0.1"),
117 kwargs.get("host", "127.0.0.1"),
118 kwargs.get("port", "8095"))
118 kwargs.get("port", "8095"))
119 self.set_credentials(kwargs.get("user", ""),
119 self.set_credentials(kwargs.get("user", ""),
120 kwargs.get("passwd", ""))
120 kwargs.get("passwd", ""))
121 self._version = kwargs.get("version", "latest")
121 self._version = kwargs.get("version", "latest")
122 self._url_list = None
122 self._url_list = None
123 self._appname = "crowd"
123 self._appname = "crowd"
124
124
125 def set_credentials(self, user, passwd):
125 def set_credentials(self, user, passwd):
126 self.user = user
126 self.user = user
127 self.passwd = passwd
127 self.passwd = passwd
128 self._make_opener()
128 self._make_opener()
129
129
130 def _make_opener(self):
130 def _make_opener(self):
131 mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
131 mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
132 mgr.add_password(None, self._uri, self.user, self.passwd)
132 mgr.add_password(None, self._uri, self.user, self.passwd)
133 handler = urllib2.HTTPBasicAuthHandler(mgr)
133 handler = urllib2.HTTPBasicAuthHandler(mgr)
134 self.opener = urllib2.build_opener(handler)
134 self.opener = urllib2.build_opener(handler)
135
135
136 def _request(self, url, body=None, headers=None,
136 def _request(self, url, body=None, headers=None,
137 method=None, noformat=False,
137 method=None, noformat=False,
138 empty_response_ok=False):
138 empty_response_ok=False):
139 _headers = {"Content-type": "application/json",
139 _headers = {"Content-type": "application/json",
140 "Accept": "application/json"}
140 "Accept": "application/json"}
141 if self.user and self.passwd:
141 if self.user and self.passwd:
142 authstring = base64.b64encode("%s:%s" % (self.user, self.passwd))
142 authstring = base64.b64encode("%s:%s" % (self.user, self.passwd))
143 _headers["Authorization"] = "Basic %s" % authstring
143 _headers["Authorization"] = "Basic %s" % authstring
144 if headers:
144 if headers:
145 _headers.update(headers)
145 _headers.update(headers)
146 log.debug("Sent crowd: \n%s"
146 log.debug("Sent crowd: \n%s"
147 % (formatted_json({"url": url, "body": body,
147 % (formatted_json({"url": url, "body": body,
148 "headers": _headers})))
148 "headers": _headers})))
149 request = urllib2.Request(url, body, _headers)
149 request = urllib2.Request(url, body, _headers)
150 if method:
150 if method:
151 request.get_method = lambda: method
151 request.get_method = lambda: method
152
152
153 global msg
153 global msg
154 msg = ""
154 msg = ""
155 try:
155 try:
156 rdoc = self.opener.open(request)
156 rdoc = self.opener.open(request)
157 msg = "".join(rdoc.readlines())
157 msg = "".join(rdoc.readlines())
158 if not msg and empty_response_ok:
158 if not msg and empty_response_ok:
159 rval = {}
159 rval = {}
160 rval["status"] = True
160 rval["status"] = True
161 rval["error"] = "Response body was empty"
161 rval["error"] = "Response body was empty"
162 elif not noformat:
162 elif not noformat:
163 rval = json.loads(msg)
163 rval = json.loads(msg)
164 rval["status"] = True
164 rval["status"] = True
165 else:
165 else:
166 rval = "".join(rdoc.readlines())
166 rval = "".join(rdoc.readlines())
167 except Exception as e:
167 except Exception as e:
168 if not noformat:
168 if not noformat:
169 rval = {"status": False,
169 rval = {"status": False,
170 "body": body,
170 "body": body,
171 "error": str(e) + "\n" + msg}
171 "error": str(e) + "\n" + msg}
172 else:
172 else:
173 rval = None
173 rval = None
174 return rval
174 return rval
175
175
176 def user_auth(self, username, password):
176 def user_auth(self, username, password):
177 """Authenticate a user against crowd. Returns brief information about
177 """Authenticate a user against crowd. Returns brief information about
178 the user."""
178 the user."""
179 url = ("%s/rest/usermanagement/%s/authentication?username=%s"
179 url = ("%s/rest/usermanagement/%s/authentication?username=%s"
180 % (self._uri, self._version, username))
180 % (self._uri, self._version, username))
181 body = json.dumps({"value": password})
181 body = json.dumps({"value": password})
182 return self._request(url, body)
182 return self._request(url, body)
183
183
184 def user_groups(self, username):
184 def user_groups(self, username):
185 """Retrieve a list of groups to which this user belongs."""
185 """Retrieve a list of groups to which this user belongs."""
186 url = ("%s/rest/usermanagement/%s/user/group/nested?username=%s"
186 url = ("%s/rest/usermanagement/%s/user/group/nested?username=%s"
187 % (self._uri, self._version, username))
187 % (self._uri, self._version, username))
188 return self._request(url)
188 return self._request(url)
189
189
190
190
191 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
191 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
192 _settings_unsafe_keys = ['app_password']
192 _settings_unsafe_keys = ['app_password']
193
193
194 def includeme(self, config):
194 def includeme(self, config):
195 config.add_authn_plugin(self)
195 config.add_authn_plugin(self)
196 config.add_authn_resource(self.get_id(), CrowdAuthnResource(self))
196 config.add_authn_resource(self.get_id(), CrowdAuthnResource(self))
197 config.add_view(
197 config.add_view(
198 'rhodecode.authentication.views.AuthnPluginViewBase',
198 'rhodecode.authentication.views.AuthnPluginViewBase',
199 attr='settings_get',
199 attr='settings_get',
200 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
200 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
201 request_method='GET',
201 request_method='GET',
202 route_name='auth_home',
202 route_name='auth_home',
203 context=CrowdAuthnResource)
203 context=CrowdAuthnResource)
204 config.add_view(
204 config.add_view(
205 'rhodecode.authentication.views.AuthnPluginViewBase',
205 'rhodecode.authentication.views.AuthnPluginViewBase',
206 attr='settings_post',
206 attr='settings_post',
207 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
207 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
208 request_method='POST',
208 request_method='POST',
209 route_name='auth_home',
209 route_name='auth_home',
210 context=CrowdAuthnResource)
210 context=CrowdAuthnResource)
211
211
212 def get_settings_schema(self):
212 def get_settings_schema(self):
213 return CrowdSettingsSchema()
213 return CrowdSettingsSchema()
214
214
215 def get_display_name(self):
215 def get_display_name(self):
216 return _('CROWD')
216 return _('CROWD')
217
217
218 @hybrid_property
218 @hybrid_property
219 def name(self):
219 def name(self):
220 return "crowd"
220 return "crowd"
221
221
222 def use_fake_password(self):
222 def use_fake_password(self):
223 return True
223 return True
224
224
225 def user_activation_state(self):
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 return 'hg.extern_activate.auto' in def_user_perms
227 return 'hg.extern_activate.auto' in def_user_perms
228
228
229 def auth(self, userobj, username, password, settings, **kwargs):
229 def auth(self, userobj, username, password, settings, **kwargs):
230 """
230 """
231 Given a user object (which may be null), username, a plaintext password,
231 Given a user object (which may be null), username, a plaintext password,
232 and a settings object (containing all the keys needed as listed in settings()),
232 and a settings object (containing all the keys needed as listed in settings()),
233 authenticate this user's login attempt.
233 authenticate this user's login attempt.
234
234
235 Return None on failure. On success, return a dictionary of the form:
235 Return None on failure. On success, return a dictionary of the form:
236
236
237 see: RhodeCodeAuthPluginBase.auth_func_attrs
237 see: RhodeCodeAuthPluginBase.auth_func_attrs
238 This is later validated for correctness
238 This is later validated for correctness
239 """
239 """
240 if not username or not password:
240 if not username or not password:
241 log.debug('Empty username or password skipping...')
241 log.debug('Empty username or password skipping...')
242 return None
242 return None
243
243
244 log.debug("Crowd settings: \n%s" % (formatted_json(settings)))
244 log.debug("Crowd settings: \n%s" % (formatted_json(settings)))
245 server = CrowdServer(**settings)
245 server = CrowdServer(**settings)
246 server.set_credentials(settings["app_name"], settings["app_password"])
246 server.set_credentials(settings["app_name"], settings["app_password"])
247 crowd_user = server.user_auth(username, password)
247 crowd_user = server.user_auth(username, password)
248 log.debug("Crowd returned: \n%s" % (formatted_json(crowd_user)))
248 log.debug("Crowd returned: \n%s" % (formatted_json(crowd_user)))
249 if not crowd_user["status"]:
249 if not crowd_user["status"]:
250 return None
250 return None
251
251
252 res = server.user_groups(crowd_user["name"])
252 res = server.user_groups(crowd_user["name"])
253 log.debug("Crowd groups: \n%s" % (formatted_json(res)))
253 log.debug("Crowd groups: \n%s" % (formatted_json(res)))
254 crowd_user["groups"] = [x["name"] for x in res["groups"]]
254 crowd_user["groups"] = [x["name"] for x in res["groups"]]
255
255
256 # old attrs fetched from RhodeCode database
256 # old attrs fetched from RhodeCode database
257 admin = getattr(userobj, 'admin', False)
257 admin = getattr(userobj, 'admin', False)
258 active = getattr(userobj, 'active', True)
258 active = getattr(userobj, 'active', True)
259 email = getattr(userobj, 'email', '')
259 email = getattr(userobj, 'email', '')
260 username = getattr(userobj, 'username', username)
260 username = getattr(userobj, 'username', username)
261 firstname = getattr(userobj, 'firstname', '')
261 firstname = getattr(userobj, 'firstname', '')
262 lastname = getattr(userobj, 'lastname', '')
262 lastname = getattr(userobj, 'lastname', '')
263 extern_type = getattr(userobj, 'extern_type', '')
263 extern_type = getattr(userobj, 'extern_type', '')
264
264
265 user_attrs = {
265 user_attrs = {
266 'username': username,
266 'username': username,
267 'firstname': crowd_user["first-name"] or firstname,
267 'firstname': crowd_user["first-name"] or firstname,
268 'lastname': crowd_user["last-name"] or lastname,
268 'lastname': crowd_user["last-name"] or lastname,
269 'groups': crowd_user["groups"],
269 'groups': crowd_user["groups"],
270 'email': crowd_user["email"] or email,
270 'email': crowd_user["email"] or email,
271 'admin': admin,
271 'admin': admin,
272 'active': active,
272 'active': active,
273 'active_from_extern': crowd_user.get('active'),
273 'active_from_extern': crowd_user.get('active'),
274 'extern_name': crowd_user["name"],
274 'extern_name': crowd_user["name"],
275 'extern_type': extern_type,
275 'extern_type': extern_type,
276 }
276 }
277
277
278 # set an admin if we're in admin_groups of crowd
278 # set an admin if we're in admin_groups of crowd
279 for group in settings["admin_groups"]:
279 for group in settings["admin_groups"]:
280 if group in user_attrs["groups"]:
280 if group in user_attrs["groups"]:
281 user_attrs["admin"] = True
281 user_attrs["admin"] = True
282 log.debug("Final crowd user object: \n%s" % (formatted_json(user_attrs)))
282 log.debug("Final crowd user object: \n%s" % (formatted_json(user_attrs)))
283 log.info('user %s authenticated correctly' % user_attrs['username'])
283 log.info('user %s authenticated correctly' % user_attrs['username'])
284 return user_attrs
284 return user_attrs
@@ -1,224 +1,224 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import colander
21 import colander
22 import logging
22 import logging
23
23
24 from rhodecode.translation import _
24 from rhodecode.translation import _
25 from rhodecode.authentication.base import (
25 from rhodecode.authentication.base import (
26 RhodeCodeExternalAuthPlugin, hybrid_property)
26 RhodeCodeExternalAuthPlugin, hybrid_property)
27 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
27 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
28 from rhodecode.authentication.routes import AuthnPluginResourceBase
28 from rhodecode.authentication.routes import AuthnPluginResourceBase
29 from rhodecode.lib.colander_utils import strip_whitespace
29 from rhodecode.lib.colander_utils import strip_whitespace
30 from rhodecode.lib.utils2 import str2bool, safe_unicode
30 from rhodecode.lib.utils2 import str2bool, safe_unicode
31 from rhodecode.model.db import User
31 from rhodecode.model.db import User
32
32
33
33
34 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
35
35
36
36
37 def plugin_factory(plugin_id, *args, **kwds):
37 def plugin_factory(plugin_id, *args, **kwds):
38 """
38 """
39 Factory function that is called during plugin discovery.
39 Factory function that is called during plugin discovery.
40 It returns the plugin instance.
40 It returns the plugin instance.
41 """
41 """
42 plugin = RhodeCodeAuthPlugin(plugin_id)
42 plugin = RhodeCodeAuthPlugin(plugin_id)
43 return plugin
43 return plugin
44
44
45
45
46 class HeadersAuthnResource(AuthnPluginResourceBase):
46 class HeadersAuthnResource(AuthnPluginResourceBase):
47 pass
47 pass
48
48
49
49
50 class HeadersSettingsSchema(AuthnPluginSettingsSchemaBase):
50 class HeadersSettingsSchema(AuthnPluginSettingsSchemaBase):
51 header = colander.SchemaNode(
51 header = colander.SchemaNode(
52 colander.String(),
52 colander.String(),
53 default='REMOTE_USER',
53 default='REMOTE_USER',
54 description=_('Header to extract the user from'),
54 description=_('Header to extract the user from'),
55 preparer=strip_whitespace,
55 preparer=strip_whitespace,
56 title=_('Header'),
56 title=_('Header'),
57 widget='string')
57 widget='string')
58 fallback_header = colander.SchemaNode(
58 fallback_header = colander.SchemaNode(
59 colander.String(),
59 colander.String(),
60 default='HTTP_X_FORWARDED_USER',
60 default='HTTP_X_FORWARDED_USER',
61 description=_('Header to extract the user from when main one fails'),
61 description=_('Header to extract the user from when main one fails'),
62 preparer=strip_whitespace,
62 preparer=strip_whitespace,
63 title=_('Fallback header'),
63 title=_('Fallback header'),
64 widget='string')
64 widget='string')
65 clean_username = colander.SchemaNode(
65 clean_username = colander.SchemaNode(
66 colander.Boolean(),
66 colander.Boolean(),
67 default=True,
67 default=True,
68 description=_('Perform cleaning of user, if passed user has @ in '
68 description=_('Perform cleaning of user, if passed user has @ in '
69 'username then first part before @ is taken. '
69 'username then first part before @ is taken. '
70 'If there\'s \\ in the username only the part after '
70 'If there\'s \\ in the username only the part after '
71 ' \\ is taken'),
71 ' \\ is taken'),
72 missing=False,
72 missing=False,
73 title=_('Clean username'),
73 title=_('Clean username'),
74 widget='bool')
74 widget='bool')
75
75
76
76
77 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
77 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
78
78
79 def includeme(self, config):
79 def includeme(self, config):
80 config.add_authn_plugin(self)
80 config.add_authn_plugin(self)
81 config.add_authn_resource(self.get_id(), HeadersAuthnResource(self))
81 config.add_authn_resource(self.get_id(), HeadersAuthnResource(self))
82 config.add_view(
82 config.add_view(
83 'rhodecode.authentication.views.AuthnPluginViewBase',
83 'rhodecode.authentication.views.AuthnPluginViewBase',
84 attr='settings_get',
84 attr='settings_get',
85 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
85 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
86 request_method='GET',
86 request_method='GET',
87 route_name='auth_home',
87 route_name='auth_home',
88 context=HeadersAuthnResource)
88 context=HeadersAuthnResource)
89 config.add_view(
89 config.add_view(
90 'rhodecode.authentication.views.AuthnPluginViewBase',
90 'rhodecode.authentication.views.AuthnPluginViewBase',
91 attr='settings_post',
91 attr='settings_post',
92 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
92 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
93 request_method='POST',
93 request_method='POST',
94 route_name='auth_home',
94 route_name='auth_home',
95 context=HeadersAuthnResource)
95 context=HeadersAuthnResource)
96
96
97 def get_display_name(self):
97 def get_display_name(self):
98 return _('Headers')
98 return _('Headers')
99
99
100 def get_settings_schema(self):
100 def get_settings_schema(self):
101 return HeadersSettingsSchema()
101 return HeadersSettingsSchema()
102
102
103 @hybrid_property
103 @hybrid_property
104 def name(self):
104 def name(self):
105 return 'headers'
105 return 'headers'
106
106
107 @property
107 @property
108 def is_headers_auth(self):
108 def is_headers_auth(self):
109 return True
109 return True
110
110
111 def use_fake_password(self):
111 def use_fake_password(self):
112 return True
112 return True
113
113
114 def user_activation_state(self):
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 return 'hg.extern_activate.auto' in def_user_perms
116 return 'hg.extern_activate.auto' in def_user_perms
117
117
118 def _clean_username(self, username):
118 def _clean_username(self, username):
119 # Removing realm and domain from username
119 # Removing realm and domain from username
120 username = username.split('@')[0]
120 username = username.split('@')[0]
121 username = username.rsplit('\\')[-1]
121 username = username.rsplit('\\')[-1]
122 return username
122 return username
123
123
124 def _get_username(self, environ, settings):
124 def _get_username(self, environ, settings):
125 username = None
125 username = None
126 environ = environ or {}
126 environ = environ or {}
127 if not environ:
127 if not environ:
128 log.debug('got empty environ: %s' % environ)
128 log.debug('got empty environ: %s' % environ)
129
129
130 settings = settings or {}
130 settings = settings or {}
131 if settings.get('header'):
131 if settings.get('header'):
132 header = settings.get('header')
132 header = settings.get('header')
133 username = environ.get(header)
133 username = environ.get(header)
134 log.debug('extracted %s:%s' % (header, username))
134 log.debug('extracted %s:%s' % (header, username))
135
135
136 # fallback mode
136 # fallback mode
137 if not username and settings.get('fallback_header'):
137 if not username and settings.get('fallback_header'):
138 header = settings.get('fallback_header')
138 header = settings.get('fallback_header')
139 username = environ.get(header)
139 username = environ.get(header)
140 log.debug('extracted %s:%s' % (header, username))
140 log.debug('extracted %s:%s' % (header, username))
141
141
142 if username and str2bool(settings.get('clean_username')):
142 if username and str2bool(settings.get('clean_username')):
143 log.debug('Received username `%s` from headers' % username)
143 log.debug('Received username `%s` from headers' % username)
144 username = self._clean_username(username)
144 username = self._clean_username(username)
145 log.debug('New cleanup user is:%s' % username)
145 log.debug('New cleanup user is:%s' % username)
146 return username
146 return username
147
147
148 def get_user(self, username=None, **kwargs):
148 def get_user(self, username=None, **kwargs):
149 """
149 """
150 Helper method for user fetching in plugins, by default it's using
150 Helper method for user fetching in plugins, by default it's using
151 simple fetch by username, but this method can be custimized in plugins
151 simple fetch by username, but this method can be custimized in plugins
152 eg. headers auth plugin to fetch user by environ params
152 eg. headers auth plugin to fetch user by environ params
153 :param username: username if given to fetch
153 :param username: username if given to fetch
154 :param kwargs: extra arguments needed for user fetching.
154 :param kwargs: extra arguments needed for user fetching.
155 """
155 """
156 environ = kwargs.get('environ') or {}
156 environ = kwargs.get('environ') or {}
157 settings = kwargs.get('settings') or {}
157 settings = kwargs.get('settings') or {}
158 username = self._get_username(environ, settings)
158 username = self._get_username(environ, settings)
159 # we got the username, so use default method now
159 # we got the username, so use default method now
160 return super(RhodeCodeAuthPlugin, self).get_user(username)
160 return super(RhodeCodeAuthPlugin, self).get_user(username)
161
161
162 def auth(self, userobj, username, password, settings, **kwargs):
162 def auth(self, userobj, username, password, settings, **kwargs):
163 """
163 """
164 Get's the headers_auth username (or email). It tries to get username
164 Get's the headers_auth username (or email). It tries to get username
165 from REMOTE_USER if this plugin is enabled, if that fails
165 from REMOTE_USER if this plugin is enabled, if that fails
166 it tries to get username from HTTP_X_FORWARDED_USER if fallback header
166 it tries to get username from HTTP_X_FORWARDED_USER if fallback header
167 is set. clean_username extracts the username from this data if it's
167 is set. clean_username extracts the username from this data if it's
168 having @ in it.
168 having @ in it.
169 Return None on failure. On success, return a dictionary of the form:
169 Return None on failure. On success, return a dictionary of the form:
170
170
171 see: RhodeCodeAuthPluginBase.auth_func_attrs
171 see: RhodeCodeAuthPluginBase.auth_func_attrs
172
172
173 :param userobj:
173 :param userobj:
174 :param username:
174 :param username:
175 :param password:
175 :param password:
176 :param settings:
176 :param settings:
177 :param kwargs:
177 :param kwargs:
178 """
178 """
179 environ = kwargs.get('environ')
179 environ = kwargs.get('environ')
180 if not environ:
180 if not environ:
181 log.debug('Empty environ data skipping...')
181 log.debug('Empty environ data skipping...')
182 return None
182 return None
183
183
184 if not userobj:
184 if not userobj:
185 userobj = self.get_user('', environ=environ, settings=settings)
185 userobj = self.get_user('', environ=environ, settings=settings)
186
186
187 # we don't care passed username/password for headers auth plugins.
187 # we don't care passed username/password for headers auth plugins.
188 # only way to log in is using environ
188 # only way to log in is using environ
189 username = None
189 username = None
190 if userobj:
190 if userobj:
191 username = getattr(userobj, 'username')
191 username = getattr(userobj, 'username')
192
192
193 if not username:
193 if not username:
194 # we don't have any objects in DB user doesn't exist extract
194 # we don't have any objects in DB user doesn't exist extract
195 # username from environ based on the settings
195 # username from environ based on the settings
196 username = self._get_username(environ, settings)
196 username = self._get_username(environ, settings)
197
197
198 # if cannot fetch username, it's a no-go for this plugin to proceed
198 # if cannot fetch username, it's a no-go for this plugin to proceed
199 if not username:
199 if not username:
200 return None
200 return None
201
201
202 # old attrs fetched from RhodeCode database
202 # old attrs fetched from RhodeCode database
203 admin = getattr(userobj, 'admin', False)
203 admin = getattr(userobj, 'admin', False)
204 active = getattr(userobj, 'active', True)
204 active = getattr(userobj, 'active', True)
205 email = getattr(userobj, 'email', '')
205 email = getattr(userobj, 'email', '')
206 firstname = getattr(userobj, 'firstname', '')
206 firstname = getattr(userobj, 'firstname', '')
207 lastname = getattr(userobj, 'lastname', '')
207 lastname = getattr(userobj, 'lastname', '')
208 extern_type = getattr(userobj, 'extern_type', '')
208 extern_type = getattr(userobj, 'extern_type', '')
209
209
210 user_attrs = {
210 user_attrs = {
211 'username': username,
211 'username': username,
212 'firstname': safe_unicode(firstname or username),
212 'firstname': safe_unicode(firstname or username),
213 'lastname': safe_unicode(lastname or ''),
213 'lastname': safe_unicode(lastname or ''),
214 'groups': [],
214 'groups': [],
215 'email': email or '',
215 'email': email or '',
216 'admin': admin or False,
216 'admin': admin or False,
217 'active': active,
217 'active': active,
218 'active_from_extern': True,
218 'active_from_extern': True,
219 'extern_name': username,
219 'extern_name': username,
220 'extern_type': extern_type,
220 'extern_type': extern_type,
221 }
221 }
222
222
223 log.info('user `%s` authenticated correctly' % user_attrs['username'])
223 log.info('user `%s` authenticated correctly' % user_attrs['username'])
224 return user_attrs
224 return user_attrs
@@ -1,166 +1,166 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 RhodeCode authentication plugin for Jasig CAS
22 RhodeCode authentication plugin for Jasig CAS
23 http://www.jasig.org/cas
23 http://www.jasig.org/cas
24 """
24 """
25
25
26
26
27 import colander
27 import colander
28 import logging
28 import logging
29 import rhodecode
29 import rhodecode
30 import urllib
30 import urllib
31 import urllib2
31 import urllib2
32
32
33 from rhodecode.translation import _
33 from rhodecode.translation import _
34 from rhodecode.authentication.base import (
34 from rhodecode.authentication.base import (
35 RhodeCodeExternalAuthPlugin, hybrid_property)
35 RhodeCodeExternalAuthPlugin, hybrid_property)
36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 from rhodecode.authentication.routes import AuthnPluginResourceBase
37 from rhodecode.authentication.routes import AuthnPluginResourceBase
38 from rhodecode.lib.colander_utils import strip_whitespace
38 from rhodecode.lib.colander_utils import strip_whitespace
39 from rhodecode.lib.utils2 import safe_unicode
39 from rhodecode.lib.utils2 import safe_unicode
40 from rhodecode.model.db import User
40 from rhodecode.model.db import User
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 def plugin_factory(plugin_id, *args, **kwds):
45 def plugin_factory(plugin_id, *args, **kwds):
46 """
46 """
47 Factory function that is called during plugin discovery.
47 Factory function that is called during plugin discovery.
48 It returns the plugin instance.
48 It returns the plugin instance.
49 """
49 """
50 plugin = RhodeCodeAuthPlugin(plugin_id)
50 plugin = RhodeCodeAuthPlugin(plugin_id)
51 return plugin
51 return plugin
52
52
53
53
54 class JasigCasAuthnResource(AuthnPluginResourceBase):
54 class JasigCasAuthnResource(AuthnPluginResourceBase):
55 pass
55 pass
56
56
57
57
58 class JasigCasSettingsSchema(AuthnPluginSettingsSchemaBase):
58 class JasigCasSettingsSchema(AuthnPluginSettingsSchemaBase):
59 service_url = colander.SchemaNode(
59 service_url = colander.SchemaNode(
60 colander.String(),
60 colander.String(),
61 default='https://domain.com/cas/v1/tickets',
61 default='https://domain.com/cas/v1/tickets',
62 description=_('The url of the Jasig CAS REST service'),
62 description=_('The url of the Jasig CAS REST service'),
63 preparer=strip_whitespace,
63 preparer=strip_whitespace,
64 title=_('URL'),
64 title=_('URL'),
65 widget='string')
65 widget='string')
66
66
67
67
68 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
68 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
69
69
70 def includeme(self, config):
70 def includeme(self, config):
71 config.add_authn_plugin(self)
71 config.add_authn_plugin(self)
72 config.add_authn_resource(self.get_id(), JasigCasAuthnResource(self))
72 config.add_authn_resource(self.get_id(), JasigCasAuthnResource(self))
73 config.add_view(
73 config.add_view(
74 'rhodecode.authentication.views.AuthnPluginViewBase',
74 'rhodecode.authentication.views.AuthnPluginViewBase',
75 attr='settings_get',
75 attr='settings_get',
76 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
76 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
77 request_method='GET',
77 request_method='GET',
78 route_name='auth_home',
78 route_name='auth_home',
79 context=JasigCasAuthnResource)
79 context=JasigCasAuthnResource)
80 config.add_view(
80 config.add_view(
81 'rhodecode.authentication.views.AuthnPluginViewBase',
81 'rhodecode.authentication.views.AuthnPluginViewBase',
82 attr='settings_post',
82 attr='settings_post',
83 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
83 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
84 request_method='POST',
84 request_method='POST',
85 route_name='auth_home',
85 route_name='auth_home',
86 context=JasigCasAuthnResource)
86 context=JasigCasAuthnResource)
87
87
88 def get_settings_schema(self):
88 def get_settings_schema(self):
89 return JasigCasSettingsSchema()
89 return JasigCasSettingsSchema()
90
90
91 def get_display_name(self):
91 def get_display_name(self):
92 return _('Jasig-CAS')
92 return _('Jasig-CAS')
93
93
94 @hybrid_property
94 @hybrid_property
95 def name(self):
95 def name(self):
96 return "jasig-cas"
96 return "jasig-cas"
97
97
98 @property
98 @property
99 def is_headers_auth(self):
99 def is_headers_auth(self):
100 return True
100 return True
101
101
102 def use_fake_password(self):
102 def use_fake_password(self):
103 return True
103 return True
104
104
105 def user_activation_state(self):
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 return 'hg.extern_activate.auto' in def_user_perms
107 return 'hg.extern_activate.auto' in def_user_perms
108
108
109 def auth(self, userobj, username, password, settings, **kwargs):
109 def auth(self, userobj, username, password, settings, **kwargs):
110 """
110 """
111 Given a user object (which may be null), username, a plaintext password,
111 Given a user object (which may be null), username, a plaintext password,
112 and a settings object (containing all the keys needed as listed in settings()),
112 and a settings object (containing all the keys needed as listed in settings()),
113 authenticate this user's login attempt.
113 authenticate this user's login attempt.
114
114
115 Return None on failure. On success, return a dictionary of the form:
115 Return None on failure. On success, return a dictionary of the form:
116
116
117 see: RhodeCodeAuthPluginBase.auth_func_attrs
117 see: RhodeCodeAuthPluginBase.auth_func_attrs
118 This is later validated for correctness
118 This is later validated for correctness
119 """
119 """
120 if not username or not password:
120 if not username or not password:
121 log.debug('Empty username or password skipping...')
121 log.debug('Empty username or password skipping...')
122 return None
122 return None
123
123
124 log.debug("Jasig CAS settings: %s", settings)
124 log.debug("Jasig CAS settings: %s", settings)
125 params = urllib.urlencode({'username': username, 'password': password})
125 params = urllib.urlencode({'username': username, 'password': password})
126 headers = {"Content-type": "application/x-www-form-urlencoded",
126 headers = {"Content-type": "application/x-www-form-urlencoded",
127 "Accept": "text/plain",
127 "Accept": "text/plain",
128 "User-Agent": "RhodeCode-auth-%s" % rhodecode.__version__}
128 "User-Agent": "RhodeCode-auth-%s" % rhodecode.__version__}
129 url = settings["service_url"]
129 url = settings["service_url"]
130
130
131 log.debug("Sent Jasig CAS: \n%s",
131 log.debug("Sent Jasig CAS: \n%s",
132 {"url": url, "body": params, "headers": headers})
132 {"url": url, "body": params, "headers": headers})
133 request = urllib2.Request(url, params, headers)
133 request = urllib2.Request(url, params, headers)
134 try:
134 try:
135 response = urllib2.urlopen(request)
135 response = urllib2.urlopen(request)
136 except urllib2.HTTPError as e:
136 except urllib2.HTTPError as e:
137 log.debug("HTTPError when requesting Jasig CAS (status code: %d)" % e.code)
137 log.debug("HTTPError when requesting Jasig CAS (status code: %d)" % e.code)
138 return None
138 return None
139 except urllib2.URLError as e:
139 except urllib2.URLError as e:
140 log.debug("URLError when requesting Jasig CAS url: %s " % url)
140 log.debug("URLError when requesting Jasig CAS url: %s " % url)
141 return None
141 return None
142
142
143 # old attrs fetched from RhodeCode database
143 # old attrs fetched from RhodeCode database
144 admin = getattr(userobj, 'admin', False)
144 admin = getattr(userobj, 'admin', False)
145 active = getattr(userobj, 'active', True)
145 active = getattr(userobj, 'active', True)
146 email = getattr(userobj, 'email', '')
146 email = getattr(userobj, 'email', '')
147 username = getattr(userobj, 'username', username)
147 username = getattr(userobj, 'username', username)
148 firstname = getattr(userobj, 'firstname', '')
148 firstname = getattr(userobj, 'firstname', '')
149 lastname = getattr(userobj, 'lastname', '')
149 lastname = getattr(userobj, 'lastname', '')
150 extern_type = getattr(userobj, 'extern_type', '')
150 extern_type = getattr(userobj, 'extern_type', '')
151
151
152 user_attrs = {
152 user_attrs = {
153 'username': username,
153 'username': username,
154 'firstname': safe_unicode(firstname or username),
154 'firstname': safe_unicode(firstname or username),
155 'lastname': safe_unicode(lastname or ''),
155 'lastname': safe_unicode(lastname or ''),
156 'groups': [],
156 'groups': [],
157 'email': email or '',
157 'email': email or '',
158 'admin': admin or False,
158 'admin': admin or False,
159 'active': active,
159 'active': active,
160 'active_from_extern': True,
160 'active_from_extern': True,
161 'extern_name': username,
161 'extern_name': username,
162 'extern_type': extern_type,
162 'extern_type': extern_type,
163 }
163 }
164
164
165 log.info('user %s authenticated correctly' % user_attrs['username'])
165 log.info('user %s authenticated correctly' % user_attrs['username'])
166 return user_attrs
166 return user_attrs
@@ -1,480 +1,480 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 RhodeCode authentication plugin for LDAP
22 RhodeCode authentication plugin for LDAP
23 """
23 """
24
24
25
25
26 import colander
26 import colander
27 import logging
27 import logging
28 import traceback
28 import traceback
29
29
30 from rhodecode.translation import _
30 from rhodecode.translation import _
31 from rhodecode.authentication.base import (
31 from rhodecode.authentication.base import (
32 RhodeCodeExternalAuthPlugin, chop_at, hybrid_property)
32 RhodeCodeExternalAuthPlugin, chop_at, hybrid_property)
33 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
33 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
34 from rhodecode.authentication.routes import AuthnPluginResourceBase
34 from rhodecode.authentication.routes import AuthnPluginResourceBase
35 from rhodecode.lib.colander_utils import strip_whitespace
35 from rhodecode.lib.colander_utils import strip_whitespace
36 from rhodecode.lib.exceptions import (
36 from rhodecode.lib.exceptions import (
37 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
37 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
38 )
38 )
39 from rhodecode.lib.utils2 import safe_unicode, safe_str
39 from rhodecode.lib.utils2 import safe_unicode, safe_str
40 from rhodecode.model.db import User
40 from rhodecode.model.db import User
41 from rhodecode.model.validators import Missing
41 from rhodecode.model.validators import Missing
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45 try:
45 try:
46 import ldap
46 import ldap
47 except ImportError:
47 except ImportError:
48 # means that python-ldap is not installed, we use Missing object to mark
48 # means that python-ldap is not installed, we use Missing object to mark
49 # ldap lib is Missing
49 # ldap lib is Missing
50 ldap = Missing
50 ldap = Missing
51
51
52
52
53 def plugin_factory(plugin_id, *args, **kwds):
53 def plugin_factory(plugin_id, *args, **kwds):
54 """
54 """
55 Factory function that is called during plugin discovery.
55 Factory function that is called during plugin discovery.
56 It returns the plugin instance.
56 It returns the plugin instance.
57 """
57 """
58 plugin = RhodeCodeAuthPlugin(plugin_id)
58 plugin = RhodeCodeAuthPlugin(plugin_id)
59 return plugin
59 return plugin
60
60
61
61
62 class LdapAuthnResource(AuthnPluginResourceBase):
62 class LdapAuthnResource(AuthnPluginResourceBase):
63 pass
63 pass
64
64
65
65
66 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
66 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
67 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
67 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
68 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
68 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
69 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
69 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
70
70
71 host = colander.SchemaNode(
71 host = colander.SchemaNode(
72 colander.String(),
72 colander.String(),
73 default='',
73 default='',
74 description=_('Host[s] of the LDAP Server \n'
74 description=_('Host[s] of the LDAP Server \n'
75 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
75 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
76 'Multiple servers can be specified using commas'),
76 'Multiple servers can be specified using commas'),
77 preparer=strip_whitespace,
77 preparer=strip_whitespace,
78 title=_('LDAP Host'),
78 title=_('LDAP Host'),
79 widget='string')
79 widget='string')
80 port = colander.SchemaNode(
80 port = colander.SchemaNode(
81 colander.Int(),
81 colander.Int(),
82 default=389,
82 default=389,
83 description=_('Custom port that the LDAP server is listening on. '
83 description=_('Custom port that the LDAP server is listening on. '
84 'Default value is: 389'),
84 'Default value is: 389'),
85 preparer=strip_whitespace,
85 preparer=strip_whitespace,
86 title=_('Port'),
86 title=_('Port'),
87 validator=colander.Range(min=0, max=65536),
87 validator=colander.Range(min=0, max=65536),
88 widget='int')
88 widget='int')
89 dn_user = colander.SchemaNode(
89 dn_user = colander.SchemaNode(
90 colander.String(),
90 colander.String(),
91 default='',
91 default='',
92 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
92 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
93 'e.g., cn=admin,dc=mydomain,dc=com, or '
93 'e.g., cn=admin,dc=mydomain,dc=com, or '
94 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
94 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
95 missing='',
95 missing='',
96 preparer=strip_whitespace,
96 preparer=strip_whitespace,
97 title=_('Account'),
97 title=_('Account'),
98 widget='string')
98 widget='string')
99 dn_pass = colander.SchemaNode(
99 dn_pass = colander.SchemaNode(
100 colander.String(),
100 colander.String(),
101 default='',
101 default='',
102 description=_('Password to authenticate for given user DN.'),
102 description=_('Password to authenticate for given user DN.'),
103 missing='',
103 missing='',
104 preparer=strip_whitespace,
104 preparer=strip_whitespace,
105 title=_('Password'),
105 title=_('Password'),
106 widget='password')
106 widget='password')
107 tls_kind = colander.SchemaNode(
107 tls_kind = colander.SchemaNode(
108 colander.String(),
108 colander.String(),
109 default=tls_kind_choices[0],
109 default=tls_kind_choices[0],
110 description=_('TLS Type'),
110 description=_('TLS Type'),
111 title=_('Connection Security'),
111 title=_('Connection Security'),
112 validator=colander.OneOf(tls_kind_choices),
112 validator=colander.OneOf(tls_kind_choices),
113 widget='select')
113 widget='select')
114 tls_reqcert = colander.SchemaNode(
114 tls_reqcert = colander.SchemaNode(
115 colander.String(),
115 colander.String(),
116 default=tls_reqcert_choices[0],
116 default=tls_reqcert_choices[0],
117 description=_('Require Cert over TLS?. Self-signed and custom '
117 description=_('Require Cert over TLS?. Self-signed and custom '
118 'certificates can be used when\n `RhodeCode Certificate` '
118 'certificates can be used when\n `RhodeCode Certificate` '
119 'found in admin > settings > system info page is extended.'),
119 'found in admin > settings > system info page is extended.'),
120 title=_('Certificate Checks'),
120 title=_('Certificate Checks'),
121 validator=colander.OneOf(tls_reqcert_choices),
121 validator=colander.OneOf(tls_reqcert_choices),
122 widget='select')
122 widget='select')
123 base_dn = colander.SchemaNode(
123 base_dn = colander.SchemaNode(
124 colander.String(),
124 colander.String(),
125 default='',
125 default='',
126 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
126 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
127 'in it to be replaced with current user credentials \n'
127 'in it to be replaced with current user credentials \n'
128 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
128 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
129 missing='',
129 missing='',
130 preparer=strip_whitespace,
130 preparer=strip_whitespace,
131 title=_('Base DN'),
131 title=_('Base DN'),
132 widget='string')
132 widget='string')
133 filter = colander.SchemaNode(
133 filter = colander.SchemaNode(
134 colander.String(),
134 colander.String(),
135 default='',
135 default='',
136 description=_('Filter to narrow results \n'
136 description=_('Filter to narrow results \n'
137 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
137 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
138 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
138 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
139 missing='',
139 missing='',
140 preparer=strip_whitespace,
140 preparer=strip_whitespace,
141 title=_('LDAP Search Filter'),
141 title=_('LDAP Search Filter'),
142 widget='string')
142 widget='string')
143
143
144 search_scope = colander.SchemaNode(
144 search_scope = colander.SchemaNode(
145 colander.String(),
145 colander.String(),
146 default=search_scope_choices[2],
146 default=search_scope_choices[2],
147 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
147 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
148 title=_('LDAP Search Scope'),
148 title=_('LDAP Search Scope'),
149 validator=colander.OneOf(search_scope_choices),
149 validator=colander.OneOf(search_scope_choices),
150 widget='select')
150 widget='select')
151 attr_login = colander.SchemaNode(
151 attr_login = colander.SchemaNode(
152 colander.String(),
152 colander.String(),
153 default='uid',
153 default='uid',
154 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
154 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
155 preparer=strip_whitespace,
155 preparer=strip_whitespace,
156 title=_('Login Attribute'),
156 title=_('Login Attribute'),
157 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
157 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
158 widget='string')
158 widget='string')
159 attr_firstname = colander.SchemaNode(
159 attr_firstname = colander.SchemaNode(
160 colander.String(),
160 colander.String(),
161 default='',
161 default='',
162 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
162 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
163 missing='',
163 missing='',
164 preparer=strip_whitespace,
164 preparer=strip_whitespace,
165 title=_('First Name Attribute'),
165 title=_('First Name Attribute'),
166 widget='string')
166 widget='string')
167 attr_lastname = colander.SchemaNode(
167 attr_lastname = colander.SchemaNode(
168 colander.String(),
168 colander.String(),
169 default='',
169 default='',
170 description=_('LDAP Attribute to map to last name (e.g., sn)'),
170 description=_('LDAP Attribute to map to last name (e.g., sn)'),
171 missing='',
171 missing='',
172 preparer=strip_whitespace,
172 preparer=strip_whitespace,
173 title=_('Last Name Attribute'),
173 title=_('Last Name Attribute'),
174 widget='string')
174 widget='string')
175 attr_email = colander.SchemaNode(
175 attr_email = colander.SchemaNode(
176 colander.String(),
176 colander.String(),
177 default='',
177 default='',
178 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
178 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
179 'Emails are a crucial part of RhodeCode. \n'
179 'Emails are a crucial part of RhodeCode. \n'
180 'If possible add a valid email attribute to ldap users.'),
180 'If possible add a valid email attribute to ldap users.'),
181 missing='',
181 missing='',
182 preparer=strip_whitespace,
182 preparer=strip_whitespace,
183 title=_('Email Attribute'),
183 title=_('Email Attribute'),
184 widget='string')
184 widget='string')
185
185
186
186
187 class AuthLdap(object):
187 class AuthLdap(object):
188
188
189 def _build_servers(self):
189 def _build_servers(self):
190 return ', '.join(
190 return ', '.join(
191 ["{}://{}:{}".format(
191 ["{}://{}:{}".format(
192 self.ldap_server_type, host.strip(), self.LDAP_SERVER_PORT)
192 self.ldap_server_type, host.strip(), self.LDAP_SERVER_PORT)
193 for host in self.SERVER_ADDRESSES])
193 for host in self.SERVER_ADDRESSES])
194
194
195 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
195 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
196 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
196 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
197 search_scope='SUBTREE', attr_login='uid',
197 search_scope='SUBTREE', attr_login='uid',
198 ldap_filter=None):
198 ldap_filter=None):
199 if ldap == Missing:
199 if ldap == Missing:
200 raise LdapImportError("Missing or incompatible ldap library")
200 raise LdapImportError("Missing or incompatible ldap library")
201
201
202 self.debug = False
202 self.debug = False
203 self.ldap_version = ldap_version
203 self.ldap_version = ldap_version
204 self.ldap_server_type = 'ldap'
204 self.ldap_server_type = 'ldap'
205
205
206 self.TLS_KIND = tls_kind
206 self.TLS_KIND = tls_kind
207
207
208 if self.TLS_KIND == 'LDAPS':
208 if self.TLS_KIND == 'LDAPS':
209 port = port or 689
209 port = port or 689
210 self.ldap_server_type += 's'
210 self.ldap_server_type += 's'
211
211
212 OPT_X_TLS_DEMAND = 2
212 OPT_X_TLS_DEMAND = 2
213 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
213 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
214 OPT_X_TLS_DEMAND)
214 OPT_X_TLS_DEMAND)
215 # split server into list
215 # split server into list
216 self.SERVER_ADDRESSES = server.split(',')
216 self.SERVER_ADDRESSES = server.split(',')
217 self.LDAP_SERVER_PORT = port
217 self.LDAP_SERVER_PORT = port
218
218
219 # USE FOR READ ONLY BIND TO LDAP SERVER
219 # USE FOR READ ONLY BIND TO LDAP SERVER
220 self.attr_login = attr_login
220 self.attr_login = attr_login
221
221
222 self.LDAP_BIND_DN = safe_str(bind_dn)
222 self.LDAP_BIND_DN = safe_str(bind_dn)
223 self.LDAP_BIND_PASS = safe_str(bind_pass)
223 self.LDAP_BIND_PASS = safe_str(bind_pass)
224 self.LDAP_SERVER = self._build_servers()
224 self.LDAP_SERVER = self._build_servers()
225 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
225 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
226 self.BASE_DN = safe_str(base_dn)
226 self.BASE_DN = safe_str(base_dn)
227 self.LDAP_FILTER = safe_str(ldap_filter)
227 self.LDAP_FILTER = safe_str(ldap_filter)
228
228
229 def _get_ldap_server(self):
229 def _get_ldap_server(self):
230 if self.debug:
230 if self.debug:
231 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
231 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
232 if hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
232 if hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
233 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR,
233 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR,
234 '/etc/openldap/cacerts')
234 '/etc/openldap/cacerts')
235 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
235 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
236 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
236 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
237 ldap.set_option(ldap.OPT_TIMEOUT, 20)
237 ldap.set_option(ldap.OPT_TIMEOUT, 20)
238 ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 10)
238 ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 10)
239 ldap.set_option(ldap.OPT_TIMELIMIT, 15)
239 ldap.set_option(ldap.OPT_TIMELIMIT, 15)
240 if self.TLS_KIND != 'PLAIN':
240 if self.TLS_KIND != 'PLAIN':
241 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
241 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
242 server = ldap.initialize(self.LDAP_SERVER)
242 server = ldap.initialize(self.LDAP_SERVER)
243 if self.ldap_version == 2:
243 if self.ldap_version == 2:
244 server.protocol = ldap.VERSION2
244 server.protocol = ldap.VERSION2
245 else:
245 else:
246 server.protocol = ldap.VERSION3
246 server.protocol = ldap.VERSION3
247
247
248 if self.TLS_KIND == 'START_TLS':
248 if self.TLS_KIND == 'START_TLS':
249 server.start_tls_s()
249 server.start_tls_s()
250
250
251 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
251 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
252 log.debug('Trying simple_bind with password and given login DN: %s',
252 log.debug('Trying simple_bind with password and given login DN: %s',
253 self.LDAP_BIND_DN)
253 self.LDAP_BIND_DN)
254 server.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
254 server.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
255
255
256 return server
256 return server
257
257
258 def get_uid(self, username):
258 def get_uid(self, username):
259 uid = username
259 uid = username
260 for server_addr in self.SERVER_ADDRESSES:
260 for server_addr in self.SERVER_ADDRESSES:
261 uid = chop_at(username, "@%s" % server_addr)
261 uid = chop_at(username, "@%s" % server_addr)
262 return uid
262 return uid
263
263
264 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
264 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
265 try:
265 try:
266 log.debug('Trying simple bind with %s', dn)
266 log.debug('Trying simple bind with %s', dn)
267 server.simple_bind_s(dn, safe_str(password))
267 server.simple_bind_s(dn, safe_str(password))
268 user = server.search_ext_s(
268 user = server.search_ext_s(
269 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
269 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
270 _, attrs = user
270 _, attrs = user
271 return attrs
271 return attrs
272
272
273 except ldap.INVALID_CREDENTIALS:
273 except ldap.INVALID_CREDENTIALS:
274 log.debug(
274 log.debug(
275 "LDAP rejected password for user '%s': %s, org_exc:",
275 "LDAP rejected password for user '%s': %s, org_exc:",
276 username, dn, exc_info=True)
276 username, dn, exc_info=True)
277
277
278 def authenticate_ldap(self, username, password):
278 def authenticate_ldap(self, username, password):
279 """
279 """
280 Authenticate a user via LDAP and return his/her LDAP properties.
280 Authenticate a user via LDAP and return his/her LDAP properties.
281
281
282 Raises AuthenticationError if the credentials are rejected, or
282 Raises AuthenticationError if the credentials are rejected, or
283 EnvironmentError if the LDAP server can't be reached.
283 EnvironmentError if the LDAP server can't be reached.
284
284
285 :param username: username
285 :param username: username
286 :param password: password
286 :param password: password
287 """
287 """
288
288
289 uid = self.get_uid(username)
289 uid = self.get_uid(username)
290
290
291 if not password:
291 if not password:
292 msg = "Authenticating user %s with blank password not allowed"
292 msg = "Authenticating user %s with blank password not allowed"
293 log.warning(msg, username)
293 log.warning(msg, username)
294 raise LdapPasswordError(msg)
294 raise LdapPasswordError(msg)
295 if "," in username:
295 if "," in username:
296 raise LdapUsernameError(
296 raise LdapUsernameError(
297 "invalid character `,` in username: `{}`".format(username))
297 "invalid character `,` in username: `{}`".format(username))
298 try:
298 try:
299 server = self._get_ldap_server()
299 server = self._get_ldap_server()
300 filter_ = '(&%s(%s=%s))' % (
300 filter_ = '(&%s(%s=%s))' % (
301 self.LDAP_FILTER, self.attr_login, username)
301 self.LDAP_FILTER, self.attr_login, username)
302 log.debug("Authenticating %r filter %s at %s", self.BASE_DN,
302 log.debug("Authenticating %r filter %s at %s", self.BASE_DN,
303 filter_, self.LDAP_SERVER)
303 filter_, self.LDAP_SERVER)
304 lobjects = server.search_ext_s(
304 lobjects = server.search_ext_s(
305 self.BASE_DN, self.SEARCH_SCOPE, filter_)
305 self.BASE_DN, self.SEARCH_SCOPE, filter_)
306
306
307 if not lobjects:
307 if not lobjects:
308 log.debug("No matching LDAP objects for authentication "
308 log.debug("No matching LDAP objects for authentication "
309 "of UID:'%s' username:(%s)", uid, username)
309 "of UID:'%s' username:(%s)", uid, username)
310 raise ldap.NO_SUCH_OBJECT()
310 raise ldap.NO_SUCH_OBJECT()
311
311
312 log.debug('Found matching ldap object, trying to authenticate')
312 log.debug('Found matching ldap object, trying to authenticate')
313 for (dn, _attrs) in lobjects:
313 for (dn, _attrs) in lobjects:
314 if dn is None:
314 if dn is None:
315 continue
315 continue
316
316
317 user_attrs = self.fetch_attrs_from_simple_bind(
317 user_attrs = self.fetch_attrs_from_simple_bind(
318 server, dn, username, password)
318 server, dn, username, password)
319 if user_attrs:
319 if user_attrs:
320 break
320 break
321
321
322 else:
322 else:
323 raise LdapPasswordError(
323 raise LdapPasswordError(
324 'Failed to authenticate user `{}`'
324 'Failed to authenticate user `{}`'
325 'with given password'.format(username))
325 'with given password'.format(username))
326
326
327 except ldap.NO_SUCH_OBJECT:
327 except ldap.NO_SUCH_OBJECT:
328 log.debug("LDAP says no such user '%s' (%s), org_exc:",
328 log.debug("LDAP says no such user '%s' (%s), org_exc:",
329 uid, username, exc_info=True)
329 uid, username, exc_info=True)
330 raise LdapUsernameError('Unable to find user')
330 raise LdapUsernameError('Unable to find user')
331 except ldap.SERVER_DOWN:
331 except ldap.SERVER_DOWN:
332 org_exc = traceback.format_exc()
332 org_exc = traceback.format_exc()
333 raise LdapConnectionError(
333 raise LdapConnectionError(
334 "LDAP can't access authentication "
334 "LDAP can't access authentication "
335 "server, org_exc:%s" % org_exc)
335 "server, org_exc:%s" % org_exc)
336
336
337 return dn, user_attrs
337 return dn, user_attrs
338
338
339
339
340 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
340 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
341 # used to define dynamic binding in the
341 # used to define dynamic binding in the
342 DYNAMIC_BIND_VAR = '$login'
342 DYNAMIC_BIND_VAR = '$login'
343 _settings_unsafe_keys = ['dn_pass']
343 _settings_unsafe_keys = ['dn_pass']
344
344
345 def includeme(self, config):
345 def includeme(self, config):
346 config.add_authn_plugin(self)
346 config.add_authn_plugin(self)
347 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
347 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
348 config.add_view(
348 config.add_view(
349 'rhodecode.authentication.views.AuthnPluginViewBase',
349 'rhodecode.authentication.views.AuthnPluginViewBase',
350 attr='settings_get',
350 attr='settings_get',
351 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
351 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
352 request_method='GET',
352 request_method='GET',
353 route_name='auth_home',
353 route_name='auth_home',
354 context=LdapAuthnResource)
354 context=LdapAuthnResource)
355 config.add_view(
355 config.add_view(
356 'rhodecode.authentication.views.AuthnPluginViewBase',
356 'rhodecode.authentication.views.AuthnPluginViewBase',
357 attr='settings_post',
357 attr='settings_post',
358 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
358 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
359 request_method='POST',
359 request_method='POST',
360 route_name='auth_home',
360 route_name='auth_home',
361 context=LdapAuthnResource)
361 context=LdapAuthnResource)
362
362
363 def get_settings_schema(self):
363 def get_settings_schema(self):
364 return LdapSettingsSchema()
364 return LdapSettingsSchema()
365
365
366 def get_display_name(self):
366 def get_display_name(self):
367 return _('LDAP')
367 return _('LDAP')
368
368
369 @hybrid_property
369 @hybrid_property
370 def name(self):
370 def name(self):
371 return "ldap"
371 return "ldap"
372
372
373 def use_fake_password(self):
373 def use_fake_password(self):
374 return True
374 return True
375
375
376 def user_activation_state(self):
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 return 'hg.extern_activate.auto' in def_user_perms
378 return 'hg.extern_activate.auto' in def_user_perms
379
379
380 def try_dynamic_binding(self, username, password, current_args):
380 def try_dynamic_binding(self, username, password, current_args):
381 """
381 """
382 Detects marker inside our original bind, and uses dynamic auth if
382 Detects marker inside our original bind, and uses dynamic auth if
383 present
383 present
384 """
384 """
385
385
386 org_bind = current_args['bind_dn']
386 org_bind = current_args['bind_dn']
387 passwd = current_args['bind_pass']
387 passwd = current_args['bind_pass']
388
388
389 def has_bind_marker(username):
389 def has_bind_marker(username):
390 if self.DYNAMIC_BIND_VAR in username:
390 if self.DYNAMIC_BIND_VAR in username:
391 return True
391 return True
392
392
393 # we only passed in user with "special" variable
393 # we only passed in user with "special" variable
394 if org_bind and has_bind_marker(org_bind) and not passwd:
394 if org_bind and has_bind_marker(org_bind) and not passwd:
395 log.debug('Using dynamic user/password binding for ldap '
395 log.debug('Using dynamic user/password binding for ldap '
396 'authentication. Replacing `%s` with username',
396 'authentication. Replacing `%s` with username',
397 self.DYNAMIC_BIND_VAR)
397 self.DYNAMIC_BIND_VAR)
398 current_args['bind_dn'] = org_bind.replace(
398 current_args['bind_dn'] = org_bind.replace(
399 self.DYNAMIC_BIND_VAR, username)
399 self.DYNAMIC_BIND_VAR, username)
400 current_args['bind_pass'] = password
400 current_args['bind_pass'] = password
401
401
402 return current_args
402 return current_args
403
403
404 def auth(self, userobj, username, password, settings, **kwargs):
404 def auth(self, userobj, username, password, settings, **kwargs):
405 """
405 """
406 Given a user object (which may be null), username, a plaintext password,
406 Given a user object (which may be null), username, a plaintext password,
407 and a settings object (containing all the keys needed as listed in
407 and a settings object (containing all the keys needed as listed in
408 settings()), authenticate this user's login attempt.
408 settings()), authenticate this user's login attempt.
409
409
410 Return None on failure. On success, return a dictionary of the form:
410 Return None on failure. On success, return a dictionary of the form:
411
411
412 see: RhodeCodeAuthPluginBase.auth_func_attrs
412 see: RhodeCodeAuthPluginBase.auth_func_attrs
413 This is later validated for correctness
413 This is later validated for correctness
414 """
414 """
415
415
416 if not username or not password:
416 if not username or not password:
417 log.debug('Empty username or password skipping...')
417 log.debug('Empty username or password skipping...')
418 return None
418 return None
419
419
420 ldap_args = {
420 ldap_args = {
421 'server': settings.get('host', ''),
421 'server': settings.get('host', ''),
422 'base_dn': settings.get('base_dn', ''),
422 'base_dn': settings.get('base_dn', ''),
423 'port': settings.get('port'),
423 'port': settings.get('port'),
424 'bind_dn': settings.get('dn_user'),
424 'bind_dn': settings.get('dn_user'),
425 'bind_pass': settings.get('dn_pass'),
425 'bind_pass': settings.get('dn_pass'),
426 'tls_kind': settings.get('tls_kind'),
426 'tls_kind': settings.get('tls_kind'),
427 'tls_reqcert': settings.get('tls_reqcert'),
427 'tls_reqcert': settings.get('tls_reqcert'),
428 'search_scope': settings.get('search_scope'),
428 'search_scope': settings.get('search_scope'),
429 'attr_login': settings.get('attr_login'),
429 'attr_login': settings.get('attr_login'),
430 'ldap_version': 3,
430 'ldap_version': 3,
431 'ldap_filter': settings.get('filter'),
431 'ldap_filter': settings.get('filter'),
432 }
432 }
433
433
434 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
434 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
435
435
436 log.debug('Checking for ldap authentication.')
436 log.debug('Checking for ldap authentication.')
437
437
438 try:
438 try:
439 aldap = AuthLdap(**ldap_args)
439 aldap = AuthLdap(**ldap_args)
440 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
440 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
441 log.debug('Got ldap DN response %s', user_dn)
441 log.debug('Got ldap DN response %s', user_dn)
442
442
443 def get_ldap_attr(k):
443 def get_ldap_attr(k):
444 return ldap_attrs.get(settings.get(k), [''])[0]
444 return ldap_attrs.get(settings.get(k), [''])[0]
445
445
446 # old attrs fetched from RhodeCode database
446 # old attrs fetched from RhodeCode database
447 admin = getattr(userobj, 'admin', False)
447 admin = getattr(userobj, 'admin', False)
448 active = getattr(userobj, 'active', True)
448 active = getattr(userobj, 'active', True)
449 email = getattr(userobj, 'email', '')
449 email = getattr(userobj, 'email', '')
450 username = getattr(userobj, 'username', username)
450 username = getattr(userobj, 'username', username)
451 firstname = getattr(userobj, 'firstname', '')
451 firstname = getattr(userobj, 'firstname', '')
452 lastname = getattr(userobj, 'lastname', '')
452 lastname = getattr(userobj, 'lastname', '')
453 extern_type = getattr(userobj, 'extern_type', '')
453 extern_type = getattr(userobj, 'extern_type', '')
454
454
455 groups = []
455 groups = []
456 user_attrs = {
456 user_attrs = {
457 'username': username,
457 'username': username,
458 'firstname': safe_unicode(
458 'firstname': safe_unicode(
459 get_ldap_attr('attr_firstname') or firstname),
459 get_ldap_attr('attr_firstname') or firstname),
460 'lastname': safe_unicode(
460 'lastname': safe_unicode(
461 get_ldap_attr('attr_lastname') or lastname),
461 get_ldap_attr('attr_lastname') or lastname),
462 'groups': groups,
462 'groups': groups,
463 'email': get_ldap_attr('attr_email') or email,
463 'email': get_ldap_attr('attr_email') or email,
464 'admin': admin,
464 'admin': admin,
465 'active': active,
465 'active': active,
466 'active_from_extern': None,
466 'active_from_extern': None,
467 'extern_name': user_dn,
467 'extern_name': user_dn,
468 'extern_type': extern_type,
468 'extern_type': extern_type,
469 }
469 }
470 log.debug('ldap user: %s', user_attrs)
470 log.debug('ldap user: %s', user_attrs)
471 log.info('user %s authenticated correctly', user_attrs['username'])
471 log.info('user %s authenticated correctly', user_attrs['username'])
472
472
473 return user_attrs
473 return user_attrs
474
474
475 except (LdapUsernameError, LdapPasswordError, LdapImportError):
475 except (LdapUsernameError, LdapPasswordError, LdapImportError):
476 log.exception("LDAP related exception")
476 log.exception("LDAP related exception")
477 return None
477 return None
478 except (Exception,):
478 except (Exception,):
479 log.exception("Other exception")
479 log.exception("Other exception")
480 return None
480 return None
@@ -1,142 +1,142 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 RhodeCode authentication plugin for built in internal auth
22 RhodeCode authentication plugin for built in internal auth
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 from pylons.i18n.translation import lazy_ugettext as _
27 from pylons.i18n.translation import lazy_ugettext as _
28
28
29 from rhodecode.authentication.base import RhodeCodeAuthPluginBase, hybrid_property
29 from rhodecode.authentication.base import RhodeCodeAuthPluginBase, hybrid_property
30 from rhodecode.authentication.routes import AuthnPluginResourceBase
30 from rhodecode.authentication.routes import AuthnPluginResourceBase
31 from rhodecode.lib.utils2 import safe_str
31 from rhodecode.lib.utils2 import safe_str
32 from rhodecode.model.db import User
32 from rhodecode.model.db import User
33
33
34 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
35
35
36
36
37 def plugin_factory(plugin_id, *args, **kwds):
37 def plugin_factory(plugin_id, *args, **kwds):
38 plugin = RhodeCodeAuthPlugin(plugin_id)
38 plugin = RhodeCodeAuthPlugin(plugin_id)
39 return plugin
39 return plugin
40
40
41
41
42 class RhodecodeAuthnResource(AuthnPluginResourceBase):
42 class RhodecodeAuthnResource(AuthnPluginResourceBase):
43 pass
43 pass
44
44
45
45
46 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
46 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
47
47
48 def includeme(self, config):
48 def includeme(self, config):
49 config.add_authn_plugin(self)
49 config.add_authn_plugin(self)
50 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
50 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
51 config.add_view(
51 config.add_view(
52 'rhodecode.authentication.views.AuthnPluginViewBase',
52 'rhodecode.authentication.views.AuthnPluginViewBase',
53 attr='settings_get',
53 attr='settings_get',
54 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
54 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
55 request_method='GET',
55 request_method='GET',
56 route_name='auth_home',
56 route_name='auth_home',
57 context=RhodecodeAuthnResource)
57 context=RhodecodeAuthnResource)
58 config.add_view(
58 config.add_view(
59 'rhodecode.authentication.views.AuthnPluginViewBase',
59 'rhodecode.authentication.views.AuthnPluginViewBase',
60 attr='settings_post',
60 attr='settings_post',
61 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
61 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
62 request_method='POST',
62 request_method='POST',
63 route_name='auth_home',
63 route_name='auth_home',
64 context=RhodecodeAuthnResource)
64 context=RhodecodeAuthnResource)
65
65
66 def get_display_name(self):
66 def get_display_name(self):
67 return _('Rhodecode')
67 return _('Rhodecode')
68
68
69 @hybrid_property
69 @hybrid_property
70 def name(self):
70 def name(self):
71 return "rhodecode"
71 return "rhodecode"
72
72
73 def user_activation_state(self):
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 return 'hg.register.auto_activate' in def_user_perms
75 return 'hg.register.auto_activate' in def_user_perms
76
76
77 def allows_authentication_from(
77 def allows_authentication_from(
78 self, user, allows_non_existing_user=True,
78 self, user, allows_non_existing_user=True,
79 allowed_auth_plugins=None, allowed_auth_sources=None):
79 allowed_auth_plugins=None, allowed_auth_sources=None):
80 """
80 """
81 Custom method for this auth that doesn't accept non existing users.
81 Custom method for this auth that doesn't accept non existing users.
82 We know that user exists in our database.
82 We know that user exists in our database.
83 """
83 """
84 allows_non_existing_user = False
84 allows_non_existing_user = False
85 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
85 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
86 user, allows_non_existing_user=allows_non_existing_user)
86 user, allows_non_existing_user=allows_non_existing_user)
87
87
88 def auth(self, userobj, username, password, settings, **kwargs):
88 def auth(self, userobj, username, password, settings, **kwargs):
89 if not userobj:
89 if not userobj:
90 log.debug('userobj was:%s skipping' % (userobj, ))
90 log.debug('userobj was:%s skipping' % (userobj, ))
91 return None
91 return None
92 if userobj.extern_type != self.name:
92 if userobj.extern_type != self.name:
93 log.warning(
93 log.warning(
94 "userobj:%s extern_type mismatch got:`%s` expected:`%s`" %
94 "userobj:%s extern_type mismatch got:`%s` expected:`%s`" %
95 (userobj, userobj.extern_type, self.name))
95 (userobj, userobj.extern_type, self.name))
96 return None
96 return None
97
97
98 user_attrs = {
98 user_attrs = {
99 "username": userobj.username,
99 "username": userobj.username,
100 "firstname": userobj.firstname,
100 "firstname": userobj.firstname,
101 "lastname": userobj.lastname,
101 "lastname": userobj.lastname,
102 "groups": [],
102 "groups": [],
103 "email": userobj.email,
103 "email": userobj.email,
104 "admin": userobj.admin,
104 "admin": userobj.admin,
105 "active": userobj.active,
105 "active": userobj.active,
106 "active_from_extern": userobj.active,
106 "active_from_extern": userobj.active,
107 "extern_name": userobj.user_id,
107 "extern_name": userobj.user_id,
108 "extern_type": userobj.extern_type,
108 "extern_type": userobj.extern_type,
109 }
109 }
110
110
111 log.debug("User attributes:%s" % (user_attrs, ))
111 log.debug("User attributes:%s" % (user_attrs, ))
112 if userobj.active:
112 if userobj.active:
113 from rhodecode.lib import auth
113 from rhodecode.lib import auth
114 crypto_backend = auth.crypto_backend()
114 crypto_backend = auth.crypto_backend()
115 password_encoded = safe_str(password)
115 password_encoded = safe_str(password)
116 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
116 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
117 password_encoded, userobj.password)
117 password_encoded, userobj.password)
118
118
119 if password_match and new_hash:
119 if password_match and new_hash:
120 log.debug('user %s properly authenticated, but '
120 log.debug('user %s properly authenticated, but '
121 'requires hash change to bcrypt', userobj)
121 'requires hash change to bcrypt', userobj)
122 # if password match, and we use OLD deprecated hash,
122 # if password match, and we use OLD deprecated hash,
123 # we should migrate this user hash password to the new hash
123 # we should migrate this user hash password to the new hash
124 # we store the new returned by hash_check_with_upgrade function
124 # we store the new returned by hash_check_with_upgrade function
125 user_attrs['_hash_migrate'] = new_hash
125 user_attrs['_hash_migrate'] = new_hash
126
126
127 if userobj.username == User.DEFAULT_USER and userobj.active:
127 if userobj.username == User.DEFAULT_USER and userobj.active:
128 log.info(
128 log.info(
129 'user %s authenticated correctly as anonymous user', userobj)
129 'user %s authenticated correctly as anonymous user', userobj)
130 return user_attrs
130 return user_attrs
131
131
132 elif userobj.username == username and password_match:
132 elif userobj.username == username and password_match:
133 log.info('user %s authenticated correctly', userobj)
133 log.info('user %s authenticated correctly', userobj)
134 return user_attrs
134 return user_attrs
135 log.info("user %s had a bad password when "
135 log.info("user %s had a bad password when "
136 "authenticating on this plugin", userobj)
136 "authenticating on this plugin", userobj)
137 return None
137 return None
138 else:
138 else:
139 log.warning(
139 log.warning(
140 'user `%s` failed to authenticate via %s, reason: account not '
140 'user `%s` failed to authenticate via %s, reason: account not '
141 'active.', username, self.name)
141 'active.', username, self.name)
142 return None
142 return None
@@ -1,146 +1,146 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 RhodeCode authentication token plugin for built in internal auth
22 RhodeCode authentication token plugin for built in internal auth
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 from rhodecode.translation import _
27 from rhodecode.translation import _
28 from rhodecode.authentication.base import (
28 from rhodecode.authentication.base import (
29 RhodeCodeAuthPluginBase, VCS_TYPE, hybrid_property)
29 RhodeCodeAuthPluginBase, VCS_TYPE, hybrid_property)
30 from rhodecode.authentication.routes import AuthnPluginResourceBase
30 from rhodecode.authentication.routes import AuthnPluginResourceBase
31 from rhodecode.model.db import User, UserApiKeys, Repository
31 from rhodecode.model.db import User, UserApiKeys, Repository
32
32
33
33
34 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
35
35
36
36
37 def plugin_factory(plugin_id, *args, **kwds):
37 def plugin_factory(plugin_id, *args, **kwds):
38 plugin = RhodeCodeAuthPlugin(plugin_id)
38 plugin = RhodeCodeAuthPlugin(plugin_id)
39 return plugin
39 return plugin
40
40
41
41
42 class RhodecodeAuthnResource(AuthnPluginResourceBase):
42 class RhodecodeAuthnResource(AuthnPluginResourceBase):
43 pass
43 pass
44
44
45
45
46 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
46 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
47 """
47 """
48 Enables usage of authentication tokens for vcs operations.
48 Enables usage of authentication tokens for vcs operations.
49 """
49 """
50
50
51 def includeme(self, config):
51 def includeme(self, config):
52 config.add_authn_plugin(self)
52 config.add_authn_plugin(self)
53 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
53 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
54 config.add_view(
54 config.add_view(
55 'rhodecode.authentication.views.AuthnPluginViewBase',
55 'rhodecode.authentication.views.AuthnPluginViewBase',
56 attr='settings_get',
56 attr='settings_get',
57 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
57 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
58 request_method='GET',
58 request_method='GET',
59 route_name='auth_home',
59 route_name='auth_home',
60 context=RhodecodeAuthnResource)
60 context=RhodecodeAuthnResource)
61 config.add_view(
61 config.add_view(
62 'rhodecode.authentication.views.AuthnPluginViewBase',
62 'rhodecode.authentication.views.AuthnPluginViewBase',
63 attr='settings_post',
63 attr='settings_post',
64 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
64 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
65 request_method='POST',
65 request_method='POST',
66 route_name='auth_home',
66 route_name='auth_home',
67 context=RhodecodeAuthnResource)
67 context=RhodecodeAuthnResource)
68
68
69 def get_display_name(self):
69 def get_display_name(self):
70 return _('Rhodecode Token Auth')
70 return _('Rhodecode Token Auth')
71
71
72 @hybrid_property
72 @hybrid_property
73 def name(self):
73 def name(self):
74 return "authtoken"
74 return "authtoken"
75
75
76 def user_activation_state(self):
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 return 'hg.register.auto_activate' in def_user_perms
78 return 'hg.register.auto_activate' in def_user_perms
79
79
80 def allows_authentication_from(
80 def allows_authentication_from(
81 self, user, allows_non_existing_user=True,
81 self, user, allows_non_existing_user=True,
82 allowed_auth_plugins=None, allowed_auth_sources=None):
82 allowed_auth_plugins=None, allowed_auth_sources=None):
83 """
83 """
84 Custom method for this auth that doesn't accept empty users. And also
84 Custom method for this auth that doesn't accept empty users. And also
85 allows users from all other active plugins to use it and also
85 allows users from all other active plugins to use it and also
86 authenticate against it. But only via vcs mode
86 authenticate against it. But only via vcs mode
87 """
87 """
88 from rhodecode.authentication.base import get_authn_registry
88 from rhodecode.authentication.base import get_authn_registry
89 authn_registry = get_authn_registry()
89 authn_registry = get_authn_registry()
90
90
91 active_plugins = set(
91 active_plugins = set(
92 [x.name for x in authn_registry.get_plugins_for_authentication()])
92 [x.name for x in authn_registry.get_plugins_for_authentication()])
93 active_plugins.discard(self.name)
93 active_plugins.discard(self.name)
94
94
95 allowed_auth_plugins = [self.name] + list(active_plugins)
95 allowed_auth_plugins = [self.name] + list(active_plugins)
96 # only for vcs operations
96 # only for vcs operations
97 allowed_auth_sources = [VCS_TYPE]
97 allowed_auth_sources = [VCS_TYPE]
98
98
99 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
99 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
100 user, allows_non_existing_user=False,
100 user, allows_non_existing_user=False,
101 allowed_auth_plugins=allowed_auth_plugins,
101 allowed_auth_plugins=allowed_auth_plugins,
102 allowed_auth_sources=allowed_auth_sources)
102 allowed_auth_sources=allowed_auth_sources)
103
103
104 def auth(self, userobj, username, password, settings, **kwargs):
104 def auth(self, userobj, username, password, settings, **kwargs):
105 if not userobj:
105 if not userobj:
106 log.debug('userobj was:%s skipping' % (userobj, ))
106 log.debug('userobj was:%s skipping' % (userobj, ))
107 return None
107 return None
108
108
109 user_attrs = {
109 user_attrs = {
110 "username": userobj.username,
110 "username": userobj.username,
111 "firstname": userobj.firstname,
111 "firstname": userobj.firstname,
112 "lastname": userobj.lastname,
112 "lastname": userobj.lastname,
113 "groups": [],
113 "groups": [],
114 "email": userobj.email,
114 "email": userobj.email,
115 "admin": userobj.admin,
115 "admin": userobj.admin,
116 "active": userobj.active,
116 "active": userobj.active,
117 "active_from_extern": userobj.active,
117 "active_from_extern": userobj.active,
118 "extern_name": userobj.user_id,
118 "extern_name": userobj.user_id,
119 "extern_type": userobj.extern_type,
119 "extern_type": userobj.extern_type,
120 }
120 }
121
121
122 log.debug('Authenticating user with args %s', user_attrs)
122 log.debug('Authenticating user with args %s', user_attrs)
123 if userobj.active:
123 if userobj.active:
124 # calling context repo for token scopes
124 # calling context repo for token scopes
125 scope_repo_id = None
125 scope_repo_id = None
126 if self.acl_repo_name:
126 if self.acl_repo_name:
127 repo = Repository.get_by_repo_name(self.acl_repo_name)
127 repo = Repository.get_by_repo_name(self.acl_repo_name)
128 scope_repo_id = repo.repo_id if repo else None
128 scope_repo_id = repo.repo_id if repo else None
129
129
130 token_match = userobj.authenticate_by_token(
130 token_match = userobj.authenticate_by_token(
131 password, roles=[UserApiKeys.ROLE_VCS],
131 password, roles=[UserApiKeys.ROLE_VCS],
132 scope_repo_id=scope_repo_id)
132 scope_repo_id=scope_repo_id)
133
133
134 if userobj.username == username and token_match:
134 if userobj.username == username and token_match:
135 log.info(
135 log.info(
136 'user `%s` successfully authenticated via %s',
136 'user `%s` successfully authenticated via %s',
137 user_attrs['username'], self.name)
137 user_attrs['username'], self.name)
138 return user_attrs
138 return user_attrs
139 log.error(
139 log.error(
140 'user `%s` failed to authenticate via %s, reason: bad or '
140 'user `%s` failed to authenticate via %s, reason: bad or '
141 'inactive token.', username, self.name)
141 'inactive token.', username, self.name)
142 else:
142 else:
143 log.warning(
143 log.warning(
144 'user `%s` failed to authenticate via %s, reason: account not '
144 'user `%s` failed to authenticate via %s, reason: account not '
145 'active.', username, self.name)
145 'active.', username, self.name)
146 return None
146 return None
@@ -1,4173 +1,4172 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Database Models for RhodeCode Enterprise
22 Database Models for RhodeCode Enterprise
23 """
23 """
24
24
25 import re
25 import re
26 import os
26 import os
27 import time
27 import time
28 import hashlib
28 import hashlib
29 import logging
29 import logging
30 import datetime
30 import datetime
31 import warnings
31 import warnings
32 import ipaddress
32 import ipaddress
33 import functools
33 import functools
34 import traceback
34 import traceback
35 import collections
35 import collections
36
36
37
37
38 from sqlalchemy import *
38 from sqlalchemy import *
39 from sqlalchemy.ext.declarative import declared_attr
39 from sqlalchemy.ext.declarative import declared_attr
40 from sqlalchemy.ext.hybrid import hybrid_property
40 from sqlalchemy.ext.hybrid import hybrid_property
41 from sqlalchemy.orm import (
41 from sqlalchemy.orm import (
42 relationship, joinedload, class_mapper, validates, aliased)
42 relationship, joinedload, class_mapper, validates, aliased)
43 from sqlalchemy.sql.expression import true
43 from sqlalchemy.sql.expression import true
44 from sqlalchemy.sql.functions import coalesce, count # noqa
44 from sqlalchemy.sql.functions import coalesce, count # noqa
45 from beaker.cache import cache_region
45 from beaker.cache import cache_region
46 from zope.cachedescriptors.property import Lazy as LazyProperty
46 from zope.cachedescriptors.property import Lazy as LazyProperty
47
47
48 from pyramid.threadlocal import get_current_request
48 from pyramid.threadlocal import get_current_request
49
49
50 from rhodecode.translation import _
50 from rhodecode.translation import _
51 from rhodecode.lib.vcs import get_vcs_instance
51 from rhodecode.lib.vcs import get_vcs_instance
52 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
52 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
53 from rhodecode.lib.utils2 import (
53 from rhodecode.lib.utils2 import (
54 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
54 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
55 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
55 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
56 glob2re, StrictAttributeDict, cleaned_uri)
56 glob2re, StrictAttributeDict, cleaned_uri)
57 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
57 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
58 from rhodecode.lib.ext_json import json
58 from rhodecode.lib.ext_json import json
59 from rhodecode.lib.caching_query import FromCache
59 from rhodecode.lib.caching_query import FromCache
60 from rhodecode.lib.encrypt import AESCipher
60 from rhodecode.lib.encrypt import AESCipher
61
61
62 from rhodecode.model.meta import Base, Session
62 from rhodecode.model.meta import Base, Session
63
63
64 URL_SEP = '/'
64 URL_SEP = '/'
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67 # =============================================================================
67 # =============================================================================
68 # BASE CLASSES
68 # BASE CLASSES
69 # =============================================================================
69 # =============================================================================
70
70
71 # this is propagated from .ini file rhodecode.encrypted_values.secret or
71 # this is propagated from .ini file rhodecode.encrypted_values.secret or
72 # beaker.session.secret if first is not set.
72 # beaker.session.secret if first is not set.
73 # and initialized at environment.py
73 # and initialized at environment.py
74 ENCRYPTION_KEY = None
74 ENCRYPTION_KEY = None
75
75
76 # used to sort permissions by types, '#' used here is not allowed to be in
76 # used to sort permissions by types, '#' used here is not allowed to be in
77 # usernames, and it's very early in sorted string.printable table.
77 # usernames, and it's very early in sorted string.printable table.
78 PERMISSION_TYPE_SORT = {
78 PERMISSION_TYPE_SORT = {
79 'admin': '####',
79 'admin': '####',
80 'write': '###',
80 'write': '###',
81 'read': '##',
81 'read': '##',
82 'none': '#',
82 'none': '#',
83 }
83 }
84
84
85
85
86 def display_sort(obj):
86 def display_sort(obj):
87 """
87 """
88 Sort function used to sort permissions in .permissions() function of
88 Sort function used to sort permissions in .permissions() function of
89 Repository, RepoGroup, UserGroup. Also it put the default user in front
89 Repository, RepoGroup, UserGroup. Also it put the default user in front
90 of all other resources
90 of all other resources
91 """
91 """
92
92
93 if obj.username == User.DEFAULT_USER:
93 if obj.username == User.DEFAULT_USER:
94 return '#####'
94 return '#####'
95 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
95 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
96 return prefix + obj.username
96 return prefix + obj.username
97
97
98
98
99 def _hash_key(k):
99 def _hash_key(k):
100 return md5_safe(k)
100 return md5_safe(k)
101
101
102
102
103 class EncryptedTextValue(TypeDecorator):
103 class EncryptedTextValue(TypeDecorator):
104 """
104 """
105 Special column for encrypted long text data, use like::
105 Special column for encrypted long text data, use like::
106
106
107 value = Column("encrypted_value", EncryptedValue(), nullable=False)
107 value = Column("encrypted_value", EncryptedValue(), nullable=False)
108
108
109 This column is intelligent so if value is in unencrypted form it return
109 This column is intelligent so if value is in unencrypted form it return
110 unencrypted form, but on save it always encrypts
110 unencrypted form, but on save it always encrypts
111 """
111 """
112 impl = Text
112 impl = Text
113
113
114 def process_bind_param(self, value, dialect):
114 def process_bind_param(self, value, dialect):
115 if not value:
115 if not value:
116 return value
116 return value
117 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
117 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
118 # protect against double encrypting if someone manually starts
118 # protect against double encrypting if someone manually starts
119 # doing
119 # doing
120 raise ValueError('value needs to be in unencrypted format, ie. '
120 raise ValueError('value needs to be in unencrypted format, ie. '
121 'not starting with enc$aes')
121 'not starting with enc$aes')
122 return 'enc$aes_hmac$%s' % AESCipher(
122 return 'enc$aes_hmac$%s' % AESCipher(
123 ENCRYPTION_KEY, hmac=True).encrypt(value)
123 ENCRYPTION_KEY, hmac=True).encrypt(value)
124
124
125 def process_result_value(self, value, dialect):
125 def process_result_value(self, value, dialect):
126 import rhodecode
126 import rhodecode
127
127
128 if not value:
128 if not value:
129 return value
129 return value
130
130
131 parts = value.split('$', 3)
131 parts = value.split('$', 3)
132 if not len(parts) == 3:
132 if not len(parts) == 3:
133 # probably not encrypted values
133 # probably not encrypted values
134 return value
134 return value
135 else:
135 else:
136 if parts[0] != 'enc':
136 if parts[0] != 'enc':
137 # parts ok but without our header ?
137 # parts ok but without our header ?
138 return value
138 return value
139 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
139 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
140 'rhodecode.encrypted_values.strict') or True)
140 'rhodecode.encrypted_values.strict') or True)
141 # at that stage we know it's our encryption
141 # at that stage we know it's our encryption
142 if parts[1] == 'aes':
142 if parts[1] == 'aes':
143 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
143 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
144 elif parts[1] == 'aes_hmac':
144 elif parts[1] == 'aes_hmac':
145 decrypted_data = AESCipher(
145 decrypted_data = AESCipher(
146 ENCRYPTION_KEY, hmac=True,
146 ENCRYPTION_KEY, hmac=True,
147 strict_verification=enc_strict_mode).decrypt(parts[2])
147 strict_verification=enc_strict_mode).decrypt(parts[2])
148 else:
148 else:
149 raise ValueError(
149 raise ValueError(
150 'Encryption type part is wrong, must be `aes` '
150 'Encryption type part is wrong, must be `aes` '
151 'or `aes_hmac`, got `%s` instead' % (parts[1]))
151 'or `aes_hmac`, got `%s` instead' % (parts[1]))
152 return decrypted_data
152 return decrypted_data
153
153
154
154
155 class BaseModel(object):
155 class BaseModel(object):
156 """
156 """
157 Base Model for all classes
157 Base Model for all classes
158 """
158 """
159
159
160 @classmethod
160 @classmethod
161 def _get_keys(cls):
161 def _get_keys(cls):
162 """return column names for this model """
162 """return column names for this model """
163 return class_mapper(cls).c.keys()
163 return class_mapper(cls).c.keys()
164
164
165 def get_dict(self):
165 def get_dict(self):
166 """
166 """
167 return dict with keys and values corresponding
167 return dict with keys and values corresponding
168 to this model data """
168 to this model data """
169
169
170 d = {}
170 d = {}
171 for k in self._get_keys():
171 for k in self._get_keys():
172 d[k] = getattr(self, k)
172 d[k] = getattr(self, k)
173
173
174 # also use __json__() if present to get additional fields
174 # also use __json__() if present to get additional fields
175 _json_attr = getattr(self, '__json__', None)
175 _json_attr = getattr(self, '__json__', None)
176 if _json_attr:
176 if _json_attr:
177 # update with attributes from __json__
177 # update with attributes from __json__
178 if callable(_json_attr):
178 if callable(_json_attr):
179 _json_attr = _json_attr()
179 _json_attr = _json_attr()
180 for k, val in _json_attr.iteritems():
180 for k, val in _json_attr.iteritems():
181 d[k] = val
181 d[k] = val
182 return d
182 return d
183
183
184 def get_appstruct(self):
184 def get_appstruct(self):
185 """return list with keys and values tuples corresponding
185 """return list with keys and values tuples corresponding
186 to this model data """
186 to this model data """
187
187
188 l = []
188 l = []
189 for k in self._get_keys():
189 for k in self._get_keys():
190 l.append((k, getattr(self, k),))
190 l.append((k, getattr(self, k),))
191 return l
191 return l
192
192
193 def populate_obj(self, populate_dict):
193 def populate_obj(self, populate_dict):
194 """populate model with data from given populate_dict"""
194 """populate model with data from given populate_dict"""
195
195
196 for k in self._get_keys():
196 for k in self._get_keys():
197 if k in populate_dict:
197 if k in populate_dict:
198 setattr(self, k, populate_dict[k])
198 setattr(self, k, populate_dict[k])
199
199
200 @classmethod
200 @classmethod
201 def query(cls):
201 def query(cls):
202 return Session().query(cls)
202 return Session().query(cls)
203
203
204 @classmethod
204 @classmethod
205 def get(cls, id_):
205 def get(cls, id_):
206 if id_:
206 if id_:
207 return cls.query().get(id_)
207 return cls.query().get(id_)
208
208
209 @classmethod
209 @classmethod
210 def get_or_404(cls, id_):
210 def get_or_404(cls, id_):
211 from pyramid.httpexceptions import HTTPNotFound
211 from pyramid.httpexceptions import HTTPNotFound
212
212
213 try:
213 try:
214 id_ = int(id_)
214 id_ = int(id_)
215 except (TypeError, ValueError):
215 except (TypeError, ValueError):
216 raise HTTPNotFound()
216 raise HTTPNotFound()
217
217
218 res = cls.query().get(id_)
218 res = cls.query().get(id_)
219 if not res:
219 if not res:
220 raise HTTPNotFound()
220 raise HTTPNotFound()
221 return res
221 return res
222
222
223 @classmethod
223 @classmethod
224 def getAll(cls):
224 def getAll(cls):
225 # deprecated and left for backward compatibility
225 # deprecated and left for backward compatibility
226 return cls.get_all()
226 return cls.get_all()
227
227
228 @classmethod
228 @classmethod
229 def get_all(cls):
229 def get_all(cls):
230 return cls.query().all()
230 return cls.query().all()
231
231
232 @classmethod
232 @classmethod
233 def delete(cls, id_):
233 def delete(cls, id_):
234 obj = cls.query().get(id_)
234 obj = cls.query().get(id_)
235 Session().delete(obj)
235 Session().delete(obj)
236
236
237 @classmethod
237 @classmethod
238 def identity_cache(cls, session, attr_name, value):
238 def identity_cache(cls, session, attr_name, value):
239 exist_in_session = []
239 exist_in_session = []
240 for (item_cls, pkey), instance in session.identity_map.items():
240 for (item_cls, pkey), instance in session.identity_map.items():
241 if cls == item_cls and getattr(instance, attr_name) == value:
241 if cls == item_cls and getattr(instance, attr_name) == value:
242 exist_in_session.append(instance)
242 exist_in_session.append(instance)
243 if exist_in_session:
243 if exist_in_session:
244 if len(exist_in_session) == 1:
244 if len(exist_in_session) == 1:
245 return exist_in_session[0]
245 return exist_in_session[0]
246 log.exception(
246 log.exception(
247 'multiple objects with attr %s and '
247 'multiple objects with attr %s and '
248 'value %s found with same name: %r',
248 'value %s found with same name: %r',
249 attr_name, value, exist_in_session)
249 attr_name, value, exist_in_session)
250
250
251 def __repr__(self):
251 def __repr__(self):
252 if hasattr(self, '__unicode__'):
252 if hasattr(self, '__unicode__'):
253 # python repr needs to return str
253 # python repr needs to return str
254 try:
254 try:
255 return safe_str(self.__unicode__())
255 return safe_str(self.__unicode__())
256 except UnicodeDecodeError:
256 except UnicodeDecodeError:
257 pass
257 pass
258 return '<DB:%s>' % (self.__class__.__name__)
258 return '<DB:%s>' % (self.__class__.__name__)
259
259
260
260
261 class RhodeCodeSetting(Base, BaseModel):
261 class RhodeCodeSetting(Base, BaseModel):
262 __tablename__ = 'rhodecode_settings'
262 __tablename__ = 'rhodecode_settings'
263 __table_args__ = (
263 __table_args__ = (
264 UniqueConstraint('app_settings_name'),
264 UniqueConstraint('app_settings_name'),
265 {'extend_existing': True, 'mysql_engine': 'InnoDB',
265 {'extend_existing': True, 'mysql_engine': 'InnoDB',
266 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
266 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
267 )
267 )
268
268
269 SETTINGS_TYPES = {
269 SETTINGS_TYPES = {
270 'str': safe_str,
270 'str': safe_str,
271 'int': safe_int,
271 'int': safe_int,
272 'unicode': safe_unicode,
272 'unicode': safe_unicode,
273 'bool': str2bool,
273 'bool': str2bool,
274 'list': functools.partial(aslist, sep=',')
274 'list': functools.partial(aslist, sep=',')
275 }
275 }
276 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
276 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
277 GLOBAL_CONF_KEY = 'app_settings'
277 GLOBAL_CONF_KEY = 'app_settings'
278
278
279 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
279 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
280 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
280 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
281 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
281 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
282 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
282 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
283
283
284 def __init__(self, key='', val='', type='unicode'):
284 def __init__(self, key='', val='', type='unicode'):
285 self.app_settings_name = key
285 self.app_settings_name = key
286 self.app_settings_type = type
286 self.app_settings_type = type
287 self.app_settings_value = val
287 self.app_settings_value = val
288
288
289 @validates('_app_settings_value')
289 @validates('_app_settings_value')
290 def validate_settings_value(self, key, val):
290 def validate_settings_value(self, key, val):
291 assert type(val) == unicode
291 assert type(val) == unicode
292 return val
292 return val
293
293
294 @hybrid_property
294 @hybrid_property
295 def app_settings_value(self):
295 def app_settings_value(self):
296 v = self._app_settings_value
296 v = self._app_settings_value
297 _type = self.app_settings_type
297 _type = self.app_settings_type
298 if _type:
298 if _type:
299 _type = self.app_settings_type.split('.')[0]
299 _type = self.app_settings_type.split('.')[0]
300 # decode the encrypted value
300 # decode the encrypted value
301 if 'encrypted' in self.app_settings_type:
301 if 'encrypted' in self.app_settings_type:
302 cipher = EncryptedTextValue()
302 cipher = EncryptedTextValue()
303 v = safe_unicode(cipher.process_result_value(v, None))
303 v = safe_unicode(cipher.process_result_value(v, None))
304
304
305 converter = self.SETTINGS_TYPES.get(_type) or \
305 converter = self.SETTINGS_TYPES.get(_type) or \
306 self.SETTINGS_TYPES['unicode']
306 self.SETTINGS_TYPES['unicode']
307 return converter(v)
307 return converter(v)
308
308
309 @app_settings_value.setter
309 @app_settings_value.setter
310 def app_settings_value(self, val):
310 def app_settings_value(self, val):
311 """
311 """
312 Setter that will always make sure we use unicode in app_settings_value
312 Setter that will always make sure we use unicode in app_settings_value
313
313
314 :param val:
314 :param val:
315 """
315 """
316 val = safe_unicode(val)
316 val = safe_unicode(val)
317 # encode the encrypted value
317 # encode the encrypted value
318 if 'encrypted' in self.app_settings_type:
318 if 'encrypted' in self.app_settings_type:
319 cipher = EncryptedTextValue()
319 cipher = EncryptedTextValue()
320 val = safe_unicode(cipher.process_bind_param(val, None))
320 val = safe_unicode(cipher.process_bind_param(val, None))
321 self._app_settings_value = val
321 self._app_settings_value = val
322
322
323 @hybrid_property
323 @hybrid_property
324 def app_settings_type(self):
324 def app_settings_type(self):
325 return self._app_settings_type
325 return self._app_settings_type
326
326
327 @app_settings_type.setter
327 @app_settings_type.setter
328 def app_settings_type(self, val):
328 def app_settings_type(self, val):
329 if val.split('.')[0] not in self.SETTINGS_TYPES:
329 if val.split('.')[0] not in self.SETTINGS_TYPES:
330 raise Exception('type must be one of %s got %s'
330 raise Exception('type must be one of %s got %s'
331 % (self.SETTINGS_TYPES.keys(), val))
331 % (self.SETTINGS_TYPES.keys(), val))
332 self._app_settings_type = val
332 self._app_settings_type = val
333
333
334 def __unicode__(self):
334 def __unicode__(self):
335 return u"<%s('%s:%s[%s]')>" % (
335 return u"<%s('%s:%s[%s]')>" % (
336 self.__class__.__name__,
336 self.__class__.__name__,
337 self.app_settings_name, self.app_settings_value,
337 self.app_settings_name, self.app_settings_value,
338 self.app_settings_type
338 self.app_settings_type
339 )
339 )
340
340
341
341
342 class RhodeCodeUi(Base, BaseModel):
342 class RhodeCodeUi(Base, BaseModel):
343 __tablename__ = 'rhodecode_ui'
343 __tablename__ = 'rhodecode_ui'
344 __table_args__ = (
344 __table_args__ = (
345 UniqueConstraint('ui_key'),
345 UniqueConstraint('ui_key'),
346 {'extend_existing': True, 'mysql_engine': 'InnoDB',
346 {'extend_existing': True, 'mysql_engine': 'InnoDB',
347 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
347 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
348 )
348 )
349
349
350 HOOK_REPO_SIZE = 'changegroup.repo_size'
350 HOOK_REPO_SIZE = 'changegroup.repo_size'
351 # HG
351 # HG
352 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
352 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
353 HOOK_PULL = 'outgoing.pull_logger'
353 HOOK_PULL = 'outgoing.pull_logger'
354 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
354 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
355 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
355 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
356 HOOK_PUSH = 'changegroup.push_logger'
356 HOOK_PUSH = 'changegroup.push_logger'
357 HOOK_PUSH_KEY = 'pushkey.key_push'
357 HOOK_PUSH_KEY = 'pushkey.key_push'
358
358
359 # TODO: johbo: Unify way how hooks are configured for git and hg,
359 # TODO: johbo: Unify way how hooks are configured for git and hg,
360 # git part is currently hardcoded.
360 # git part is currently hardcoded.
361
361
362 # SVN PATTERNS
362 # SVN PATTERNS
363 SVN_BRANCH_ID = 'vcs_svn_branch'
363 SVN_BRANCH_ID = 'vcs_svn_branch'
364 SVN_TAG_ID = 'vcs_svn_tag'
364 SVN_TAG_ID = 'vcs_svn_tag'
365
365
366 ui_id = Column(
366 ui_id = Column(
367 "ui_id", Integer(), nullable=False, unique=True, default=None,
367 "ui_id", Integer(), nullable=False, unique=True, default=None,
368 primary_key=True)
368 primary_key=True)
369 ui_section = Column(
369 ui_section = Column(
370 "ui_section", String(255), nullable=True, unique=None, default=None)
370 "ui_section", String(255), nullable=True, unique=None, default=None)
371 ui_key = Column(
371 ui_key = Column(
372 "ui_key", String(255), nullable=True, unique=None, default=None)
372 "ui_key", String(255), nullable=True, unique=None, default=None)
373 ui_value = Column(
373 ui_value = Column(
374 "ui_value", String(255), nullable=True, unique=None, default=None)
374 "ui_value", String(255), nullable=True, unique=None, default=None)
375 ui_active = Column(
375 ui_active = Column(
376 "ui_active", Boolean(), nullable=True, unique=None, default=True)
376 "ui_active", Boolean(), nullable=True, unique=None, default=True)
377
377
378 def __repr__(self):
378 def __repr__(self):
379 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
379 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
380 self.ui_key, self.ui_value)
380 self.ui_key, self.ui_value)
381
381
382
382
383 class RepoRhodeCodeSetting(Base, BaseModel):
383 class RepoRhodeCodeSetting(Base, BaseModel):
384 __tablename__ = 'repo_rhodecode_settings'
384 __tablename__ = 'repo_rhodecode_settings'
385 __table_args__ = (
385 __table_args__ = (
386 UniqueConstraint(
386 UniqueConstraint(
387 'app_settings_name', 'repository_id',
387 'app_settings_name', 'repository_id',
388 name='uq_repo_rhodecode_setting_name_repo_id'),
388 name='uq_repo_rhodecode_setting_name_repo_id'),
389 {'extend_existing': True, 'mysql_engine': 'InnoDB',
389 {'extend_existing': True, 'mysql_engine': 'InnoDB',
390 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
390 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
391 )
391 )
392
392
393 repository_id = Column(
393 repository_id = Column(
394 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
394 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
395 nullable=False)
395 nullable=False)
396 app_settings_id = Column(
396 app_settings_id = Column(
397 "app_settings_id", Integer(), nullable=False, unique=True,
397 "app_settings_id", Integer(), nullable=False, unique=True,
398 default=None, primary_key=True)
398 default=None, primary_key=True)
399 app_settings_name = Column(
399 app_settings_name = Column(
400 "app_settings_name", String(255), nullable=True, unique=None,
400 "app_settings_name", String(255), nullable=True, unique=None,
401 default=None)
401 default=None)
402 _app_settings_value = Column(
402 _app_settings_value = Column(
403 "app_settings_value", String(4096), nullable=True, unique=None,
403 "app_settings_value", String(4096), nullable=True, unique=None,
404 default=None)
404 default=None)
405 _app_settings_type = Column(
405 _app_settings_type = Column(
406 "app_settings_type", String(255), nullable=True, unique=None,
406 "app_settings_type", String(255), nullable=True, unique=None,
407 default=None)
407 default=None)
408
408
409 repository = relationship('Repository')
409 repository = relationship('Repository')
410
410
411 def __init__(self, repository_id, key='', val='', type='unicode'):
411 def __init__(self, repository_id, key='', val='', type='unicode'):
412 self.repository_id = repository_id
412 self.repository_id = repository_id
413 self.app_settings_name = key
413 self.app_settings_name = key
414 self.app_settings_type = type
414 self.app_settings_type = type
415 self.app_settings_value = val
415 self.app_settings_value = val
416
416
417 @validates('_app_settings_value')
417 @validates('_app_settings_value')
418 def validate_settings_value(self, key, val):
418 def validate_settings_value(self, key, val):
419 assert type(val) == unicode
419 assert type(val) == unicode
420 return val
420 return val
421
421
422 @hybrid_property
422 @hybrid_property
423 def app_settings_value(self):
423 def app_settings_value(self):
424 v = self._app_settings_value
424 v = self._app_settings_value
425 type_ = self.app_settings_type
425 type_ = self.app_settings_type
426 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
426 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
427 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
427 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
428 return converter(v)
428 return converter(v)
429
429
430 @app_settings_value.setter
430 @app_settings_value.setter
431 def app_settings_value(self, val):
431 def app_settings_value(self, val):
432 """
432 """
433 Setter that will always make sure we use unicode in app_settings_value
433 Setter that will always make sure we use unicode in app_settings_value
434
434
435 :param val:
435 :param val:
436 """
436 """
437 self._app_settings_value = safe_unicode(val)
437 self._app_settings_value = safe_unicode(val)
438
438
439 @hybrid_property
439 @hybrid_property
440 def app_settings_type(self):
440 def app_settings_type(self):
441 return self._app_settings_type
441 return self._app_settings_type
442
442
443 @app_settings_type.setter
443 @app_settings_type.setter
444 def app_settings_type(self, val):
444 def app_settings_type(self, val):
445 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
445 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
446 if val not in SETTINGS_TYPES:
446 if val not in SETTINGS_TYPES:
447 raise Exception('type must be one of %s got %s'
447 raise Exception('type must be one of %s got %s'
448 % (SETTINGS_TYPES.keys(), val))
448 % (SETTINGS_TYPES.keys(), val))
449 self._app_settings_type = val
449 self._app_settings_type = val
450
450
451 def __unicode__(self):
451 def __unicode__(self):
452 return u"<%s('%s:%s:%s[%s]')>" % (
452 return u"<%s('%s:%s:%s[%s]')>" % (
453 self.__class__.__name__, self.repository.repo_name,
453 self.__class__.__name__, self.repository.repo_name,
454 self.app_settings_name, self.app_settings_value,
454 self.app_settings_name, self.app_settings_value,
455 self.app_settings_type
455 self.app_settings_type
456 )
456 )
457
457
458
458
459 class RepoRhodeCodeUi(Base, BaseModel):
459 class RepoRhodeCodeUi(Base, BaseModel):
460 __tablename__ = 'repo_rhodecode_ui'
460 __tablename__ = 'repo_rhodecode_ui'
461 __table_args__ = (
461 __table_args__ = (
462 UniqueConstraint(
462 UniqueConstraint(
463 'repository_id', 'ui_section', 'ui_key',
463 'repository_id', 'ui_section', 'ui_key',
464 name='uq_repo_rhodecode_ui_repository_id_section_key'),
464 name='uq_repo_rhodecode_ui_repository_id_section_key'),
465 {'extend_existing': True, 'mysql_engine': 'InnoDB',
465 {'extend_existing': True, 'mysql_engine': 'InnoDB',
466 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
466 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
467 )
467 )
468
468
469 repository_id = Column(
469 repository_id = Column(
470 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
470 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
471 nullable=False)
471 nullable=False)
472 ui_id = Column(
472 ui_id = Column(
473 "ui_id", Integer(), nullable=False, unique=True, default=None,
473 "ui_id", Integer(), nullable=False, unique=True, default=None,
474 primary_key=True)
474 primary_key=True)
475 ui_section = Column(
475 ui_section = Column(
476 "ui_section", String(255), nullable=True, unique=None, default=None)
476 "ui_section", String(255), nullable=True, unique=None, default=None)
477 ui_key = Column(
477 ui_key = Column(
478 "ui_key", String(255), nullable=True, unique=None, default=None)
478 "ui_key", String(255), nullable=True, unique=None, default=None)
479 ui_value = Column(
479 ui_value = Column(
480 "ui_value", String(255), nullable=True, unique=None, default=None)
480 "ui_value", String(255), nullable=True, unique=None, default=None)
481 ui_active = Column(
481 ui_active = Column(
482 "ui_active", Boolean(), nullable=True, unique=None, default=True)
482 "ui_active", Boolean(), nullable=True, unique=None, default=True)
483
483
484 repository = relationship('Repository')
484 repository = relationship('Repository')
485
485
486 def __repr__(self):
486 def __repr__(self):
487 return '<%s[%s:%s]%s=>%s]>' % (
487 return '<%s[%s:%s]%s=>%s]>' % (
488 self.__class__.__name__, self.repository.repo_name,
488 self.__class__.__name__, self.repository.repo_name,
489 self.ui_section, self.ui_key, self.ui_value)
489 self.ui_section, self.ui_key, self.ui_value)
490
490
491
491
492 class User(Base, BaseModel):
492 class User(Base, BaseModel):
493 __tablename__ = 'users'
493 __tablename__ = 'users'
494 __table_args__ = (
494 __table_args__ = (
495 UniqueConstraint('username'), UniqueConstraint('email'),
495 UniqueConstraint('username'), UniqueConstraint('email'),
496 Index('u_username_idx', 'username'),
496 Index('u_username_idx', 'username'),
497 Index('u_email_idx', 'email'),
497 Index('u_email_idx', 'email'),
498 {'extend_existing': True, 'mysql_engine': 'InnoDB',
498 {'extend_existing': True, 'mysql_engine': 'InnoDB',
499 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
499 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
500 )
500 )
501 DEFAULT_USER = 'default'
501 DEFAULT_USER = 'default'
502 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
502 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
503 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
503 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
504
504
505 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
505 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
506 username = Column("username", String(255), nullable=True, unique=None, default=None)
506 username = Column("username", String(255), nullable=True, unique=None, default=None)
507 password = Column("password", String(255), nullable=True, unique=None, default=None)
507 password = Column("password", String(255), nullable=True, unique=None, default=None)
508 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
508 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
509 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
509 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
510 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
510 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
511 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
511 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
512 _email = Column("email", String(255), nullable=True, unique=None, default=None)
512 _email = Column("email", String(255), nullable=True, unique=None, default=None)
513 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
513 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
514 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
514 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
515
515
516 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
516 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
517 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
517 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
518 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
518 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
519 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
519 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
520 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
520 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
521 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
521 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
522
522
523 user_log = relationship('UserLog')
523 user_log = relationship('UserLog')
524 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
524 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
525
525
526 repositories = relationship('Repository')
526 repositories = relationship('Repository')
527 repository_groups = relationship('RepoGroup')
527 repository_groups = relationship('RepoGroup')
528 user_groups = relationship('UserGroup')
528 user_groups = relationship('UserGroup')
529
529
530 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
530 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
531 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
531 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
532
532
533 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
533 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
534 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
534 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
535 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
535 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
536
536
537 group_member = relationship('UserGroupMember', cascade='all')
537 group_member = relationship('UserGroupMember', cascade='all')
538
538
539 notifications = relationship('UserNotification', cascade='all')
539 notifications = relationship('UserNotification', cascade='all')
540 # notifications assigned to this user
540 # notifications assigned to this user
541 user_created_notifications = relationship('Notification', cascade='all')
541 user_created_notifications = relationship('Notification', cascade='all')
542 # comments created by this user
542 # comments created by this user
543 user_comments = relationship('ChangesetComment', cascade='all')
543 user_comments = relationship('ChangesetComment', cascade='all')
544 # user profile extra info
544 # user profile extra info
545 user_emails = relationship('UserEmailMap', cascade='all')
545 user_emails = relationship('UserEmailMap', cascade='all')
546 user_ip_map = relationship('UserIpMap', cascade='all')
546 user_ip_map = relationship('UserIpMap', cascade='all')
547 user_auth_tokens = relationship('UserApiKeys', cascade='all')
547 user_auth_tokens = relationship('UserApiKeys', cascade='all')
548 user_ssh_keys = relationship('UserSshKeys', cascade='all')
548 user_ssh_keys = relationship('UserSshKeys', cascade='all')
549
549
550 # gists
550 # gists
551 user_gists = relationship('Gist', cascade='all')
551 user_gists = relationship('Gist', cascade='all')
552 # user pull requests
552 # user pull requests
553 user_pull_requests = relationship('PullRequest', cascade='all')
553 user_pull_requests = relationship('PullRequest', cascade='all')
554 # external identities
554 # external identities
555 extenal_identities = relationship(
555 extenal_identities = relationship(
556 'ExternalIdentity',
556 'ExternalIdentity',
557 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
557 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
558 cascade='all')
558 cascade='all')
559
559
560 def __unicode__(self):
560 def __unicode__(self):
561 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
561 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
562 self.user_id, self.username)
562 self.user_id, self.username)
563
563
564 @hybrid_property
564 @hybrid_property
565 def email(self):
565 def email(self):
566 return self._email
566 return self._email
567
567
568 @email.setter
568 @email.setter
569 def email(self, val):
569 def email(self, val):
570 self._email = val.lower() if val else None
570 self._email = val.lower() if val else None
571
571
572 @hybrid_property
572 @hybrid_property
573 def first_name(self):
573 def first_name(self):
574 from rhodecode.lib import helpers as h
574 from rhodecode.lib import helpers as h
575 if self.name:
575 if self.name:
576 return h.escape(self.name)
576 return h.escape(self.name)
577 return self.name
577 return self.name
578
578
579 @hybrid_property
579 @hybrid_property
580 def last_name(self):
580 def last_name(self):
581 from rhodecode.lib import helpers as h
581 from rhodecode.lib import helpers as h
582 if self.lastname:
582 if self.lastname:
583 return h.escape(self.lastname)
583 return h.escape(self.lastname)
584 return self.lastname
584 return self.lastname
585
585
586 @hybrid_property
586 @hybrid_property
587 def api_key(self):
587 def api_key(self):
588 """
588 """
589 Fetch if exist an auth-token with role ALL connected to this user
589 Fetch if exist an auth-token with role ALL connected to this user
590 """
590 """
591 user_auth_token = UserApiKeys.query()\
591 user_auth_token = UserApiKeys.query()\
592 .filter(UserApiKeys.user_id == self.user_id)\
592 .filter(UserApiKeys.user_id == self.user_id)\
593 .filter(or_(UserApiKeys.expires == -1,
593 .filter(or_(UserApiKeys.expires == -1,
594 UserApiKeys.expires >= time.time()))\
594 UserApiKeys.expires >= time.time()))\
595 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
595 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
596 if user_auth_token:
596 if user_auth_token:
597 user_auth_token = user_auth_token.api_key
597 user_auth_token = user_auth_token.api_key
598
598
599 return user_auth_token
599 return user_auth_token
600
600
601 @api_key.setter
601 @api_key.setter
602 def api_key(self, val):
602 def api_key(self, val):
603 # don't allow to set API key this is deprecated for now
603 # don't allow to set API key this is deprecated for now
604 self._api_key = None
604 self._api_key = None
605
605
606 @property
606 @property
607 def reviewer_pull_requests(self):
607 def reviewer_pull_requests(self):
608 return PullRequestReviewers.query() \
608 return PullRequestReviewers.query() \
609 .options(joinedload(PullRequestReviewers.pull_request)) \
609 .options(joinedload(PullRequestReviewers.pull_request)) \
610 .filter(PullRequestReviewers.user_id == self.user_id) \
610 .filter(PullRequestReviewers.user_id == self.user_id) \
611 .all()
611 .all()
612
612
613 @property
613 @property
614 def firstname(self):
614 def firstname(self):
615 # alias for future
615 # alias for future
616 return self.name
616 return self.name
617
617
618 @property
618 @property
619 def emails(self):
619 def emails(self):
620 other = UserEmailMap.query()\
620 other = UserEmailMap.query()\
621 .filter(UserEmailMap.user == self) \
621 .filter(UserEmailMap.user == self) \
622 .order_by(UserEmailMap.email_id.asc()) \
622 .order_by(UserEmailMap.email_id.asc()) \
623 .all()
623 .all()
624 return [self.email] + [x.email for x in other]
624 return [self.email] + [x.email for x in other]
625
625
626 @property
626 @property
627 def auth_tokens(self):
627 def auth_tokens(self):
628 auth_tokens = self.get_auth_tokens()
628 auth_tokens = self.get_auth_tokens()
629 return [x.api_key for x in auth_tokens]
629 return [x.api_key for x in auth_tokens]
630
630
631 def get_auth_tokens(self):
631 def get_auth_tokens(self):
632 return UserApiKeys.query()\
632 return UserApiKeys.query()\
633 .filter(UserApiKeys.user == self)\
633 .filter(UserApiKeys.user == self)\
634 .order_by(UserApiKeys.user_api_key_id.asc())\
634 .order_by(UserApiKeys.user_api_key_id.asc())\
635 .all()
635 .all()
636
636
637 @property
637 @property
638 def feed_token(self):
638 def feed_token(self):
639 return self.get_feed_token()
639 return self.get_feed_token()
640
640
641 def get_feed_token(self):
641 def get_feed_token(self):
642 feed_tokens = UserApiKeys.query()\
642 feed_tokens = UserApiKeys.query()\
643 .filter(UserApiKeys.user == self)\
643 .filter(UserApiKeys.user == self)\
644 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
644 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
645 .all()
645 .all()
646 if feed_tokens:
646 if feed_tokens:
647 return feed_tokens[0].api_key
647 return feed_tokens[0].api_key
648 return 'NO_FEED_TOKEN_AVAILABLE'
648 return 'NO_FEED_TOKEN_AVAILABLE'
649
649
650 @classmethod
650 @classmethod
651 def extra_valid_auth_tokens(cls, user, role=None):
651 def extra_valid_auth_tokens(cls, user, role=None):
652 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
652 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
653 .filter(or_(UserApiKeys.expires == -1,
653 .filter(or_(UserApiKeys.expires == -1,
654 UserApiKeys.expires >= time.time()))
654 UserApiKeys.expires >= time.time()))
655 if role:
655 if role:
656 tokens = tokens.filter(or_(UserApiKeys.role == role,
656 tokens = tokens.filter(or_(UserApiKeys.role == role,
657 UserApiKeys.role == UserApiKeys.ROLE_ALL))
657 UserApiKeys.role == UserApiKeys.ROLE_ALL))
658 return tokens.all()
658 return tokens.all()
659
659
660 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
660 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
661 from rhodecode.lib import auth
661 from rhodecode.lib import auth
662
662
663 log.debug('Trying to authenticate user: %s via auth-token, '
663 log.debug('Trying to authenticate user: %s via auth-token, '
664 'and roles: %s', self, roles)
664 'and roles: %s', self, roles)
665
665
666 if not auth_token:
666 if not auth_token:
667 return False
667 return False
668
668
669 crypto_backend = auth.crypto_backend()
669 crypto_backend = auth.crypto_backend()
670
670
671 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
671 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
672 tokens_q = UserApiKeys.query()\
672 tokens_q = UserApiKeys.query()\
673 .filter(UserApiKeys.user_id == self.user_id)\
673 .filter(UserApiKeys.user_id == self.user_id)\
674 .filter(or_(UserApiKeys.expires == -1,
674 .filter(or_(UserApiKeys.expires == -1,
675 UserApiKeys.expires >= time.time()))
675 UserApiKeys.expires >= time.time()))
676
676
677 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
677 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
678
678
679 plain_tokens = []
679 plain_tokens = []
680 hash_tokens = []
680 hash_tokens = []
681
681
682 for token in tokens_q.all():
682 for token in tokens_q.all():
683 # verify scope first
683 # verify scope first
684 if token.repo_id:
684 if token.repo_id:
685 # token has a scope, we need to verify it
685 # token has a scope, we need to verify it
686 if scope_repo_id != token.repo_id:
686 if scope_repo_id != token.repo_id:
687 log.debug(
687 log.debug(
688 'Scope mismatch: token has a set repo scope: %s, '
688 'Scope mismatch: token has a set repo scope: %s, '
689 'and calling scope is:%s, skipping further checks',
689 'and calling scope is:%s, skipping further checks',
690 token.repo, scope_repo_id)
690 token.repo, scope_repo_id)
691 # token has a scope, and it doesn't match, skip token
691 # token has a scope, and it doesn't match, skip token
692 continue
692 continue
693
693
694 if token.api_key.startswith(crypto_backend.ENC_PREF):
694 if token.api_key.startswith(crypto_backend.ENC_PREF):
695 hash_tokens.append(token.api_key)
695 hash_tokens.append(token.api_key)
696 else:
696 else:
697 plain_tokens.append(token.api_key)
697 plain_tokens.append(token.api_key)
698
698
699 is_plain_match = auth_token in plain_tokens
699 is_plain_match = auth_token in plain_tokens
700 if is_plain_match:
700 if is_plain_match:
701 return True
701 return True
702
702
703 for hashed in hash_tokens:
703 for hashed in hash_tokens:
704 # TODO(marcink): this is expensive to calculate, but most secure
704 # TODO(marcink): this is expensive to calculate, but most secure
705 match = crypto_backend.hash_check(auth_token, hashed)
705 match = crypto_backend.hash_check(auth_token, hashed)
706 if match:
706 if match:
707 return True
707 return True
708
708
709 return False
709 return False
710
710
711 @property
711 @property
712 def ip_addresses(self):
712 def ip_addresses(self):
713 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
713 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
714 return [x.ip_addr for x in ret]
714 return [x.ip_addr for x in ret]
715
715
716 @property
716 @property
717 def username_and_name(self):
717 def username_and_name(self):
718 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
718 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
719
719
720 @property
720 @property
721 def username_or_name_or_email(self):
721 def username_or_name_or_email(self):
722 full_name = self.full_name if self.full_name is not ' ' else None
722 full_name = self.full_name if self.full_name is not ' ' else None
723 return self.username or full_name or self.email
723 return self.username or full_name or self.email
724
724
725 @property
725 @property
726 def full_name(self):
726 def full_name(self):
727 return '%s %s' % (self.first_name, self.last_name)
727 return '%s %s' % (self.first_name, self.last_name)
728
728
729 @property
729 @property
730 def full_name_or_username(self):
730 def full_name_or_username(self):
731 return ('%s %s' % (self.first_name, self.last_name)
731 return ('%s %s' % (self.first_name, self.last_name)
732 if (self.first_name and self.last_name) else self.username)
732 if (self.first_name and self.last_name) else self.username)
733
733
734 @property
734 @property
735 def full_contact(self):
735 def full_contact(self):
736 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
736 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
737
737
738 @property
738 @property
739 def short_contact(self):
739 def short_contact(self):
740 return '%s %s' % (self.first_name, self.last_name)
740 return '%s %s' % (self.first_name, self.last_name)
741
741
742 @property
742 @property
743 def is_admin(self):
743 def is_admin(self):
744 return self.admin
744 return self.admin
745
745
746 @property
746 def AuthUser(self, **kwargs):
747 def AuthUser(self):
748 """
747 """
749 Returns instance of AuthUser for this user
748 Returns instance of AuthUser for this user
750 """
749 """
751 from rhodecode.lib.auth import AuthUser
750 from rhodecode.lib.auth import AuthUser
752 return AuthUser(user_id=self.user_id, username=self.username)
751 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
753
752
754 @hybrid_property
753 @hybrid_property
755 def user_data(self):
754 def user_data(self):
756 if not self._user_data:
755 if not self._user_data:
757 return {}
756 return {}
758
757
759 try:
758 try:
760 return json.loads(self._user_data)
759 return json.loads(self._user_data)
761 except TypeError:
760 except TypeError:
762 return {}
761 return {}
763
762
764 @user_data.setter
763 @user_data.setter
765 def user_data(self, val):
764 def user_data(self, val):
766 if not isinstance(val, dict):
765 if not isinstance(val, dict):
767 raise Exception('user_data must be dict, got %s' % type(val))
766 raise Exception('user_data must be dict, got %s' % type(val))
768 try:
767 try:
769 self._user_data = json.dumps(val)
768 self._user_data = json.dumps(val)
770 except Exception:
769 except Exception:
771 log.error(traceback.format_exc())
770 log.error(traceback.format_exc())
772
771
773 @classmethod
772 @classmethod
774 def get_by_username(cls, username, case_insensitive=False,
773 def get_by_username(cls, username, case_insensitive=False,
775 cache=False, identity_cache=False):
774 cache=False, identity_cache=False):
776 session = Session()
775 session = Session()
777
776
778 if case_insensitive:
777 if case_insensitive:
779 q = cls.query().filter(
778 q = cls.query().filter(
780 func.lower(cls.username) == func.lower(username))
779 func.lower(cls.username) == func.lower(username))
781 else:
780 else:
782 q = cls.query().filter(cls.username == username)
781 q = cls.query().filter(cls.username == username)
783
782
784 if cache:
783 if cache:
785 if identity_cache:
784 if identity_cache:
786 val = cls.identity_cache(session, 'username', username)
785 val = cls.identity_cache(session, 'username', username)
787 if val:
786 if val:
788 return val
787 return val
789 else:
788 else:
790 cache_key = "get_user_by_name_%s" % _hash_key(username)
789 cache_key = "get_user_by_name_%s" % _hash_key(username)
791 q = q.options(
790 q = q.options(
792 FromCache("sql_cache_short", cache_key))
791 FromCache("sql_cache_short", cache_key))
793
792
794 return q.scalar()
793 return q.scalar()
795
794
796 @classmethod
795 @classmethod
797 def get_by_auth_token(cls, auth_token, cache=False):
796 def get_by_auth_token(cls, auth_token, cache=False):
798 q = UserApiKeys.query()\
797 q = UserApiKeys.query()\
799 .filter(UserApiKeys.api_key == auth_token)\
798 .filter(UserApiKeys.api_key == auth_token)\
800 .filter(or_(UserApiKeys.expires == -1,
799 .filter(or_(UserApiKeys.expires == -1,
801 UserApiKeys.expires >= time.time()))
800 UserApiKeys.expires >= time.time()))
802 if cache:
801 if cache:
803 q = q.options(
802 q = q.options(
804 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
803 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
805
804
806 match = q.first()
805 match = q.first()
807 if match:
806 if match:
808 return match.user
807 return match.user
809
808
810 @classmethod
809 @classmethod
811 def get_by_email(cls, email, case_insensitive=False, cache=False):
810 def get_by_email(cls, email, case_insensitive=False, cache=False):
812
811
813 if case_insensitive:
812 if case_insensitive:
814 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
813 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
815
814
816 else:
815 else:
817 q = cls.query().filter(cls.email == email)
816 q = cls.query().filter(cls.email == email)
818
817
819 email_key = _hash_key(email)
818 email_key = _hash_key(email)
820 if cache:
819 if cache:
821 q = q.options(
820 q = q.options(
822 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
821 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
823
822
824 ret = q.scalar()
823 ret = q.scalar()
825 if ret is None:
824 if ret is None:
826 q = UserEmailMap.query()
825 q = UserEmailMap.query()
827 # try fetching in alternate email map
826 # try fetching in alternate email map
828 if case_insensitive:
827 if case_insensitive:
829 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
828 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
830 else:
829 else:
831 q = q.filter(UserEmailMap.email == email)
830 q = q.filter(UserEmailMap.email == email)
832 q = q.options(joinedload(UserEmailMap.user))
831 q = q.options(joinedload(UserEmailMap.user))
833 if cache:
832 if cache:
834 q = q.options(
833 q = q.options(
835 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
834 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
836 ret = getattr(q.scalar(), 'user', None)
835 ret = getattr(q.scalar(), 'user', None)
837
836
838 return ret
837 return ret
839
838
840 @classmethod
839 @classmethod
841 def get_from_cs_author(cls, author):
840 def get_from_cs_author(cls, author):
842 """
841 """
843 Tries to get User objects out of commit author string
842 Tries to get User objects out of commit author string
844
843
845 :param author:
844 :param author:
846 """
845 """
847 from rhodecode.lib.helpers import email, author_name
846 from rhodecode.lib.helpers import email, author_name
848 # Valid email in the attribute passed, see if they're in the system
847 # Valid email in the attribute passed, see if they're in the system
849 _email = email(author)
848 _email = email(author)
850 if _email:
849 if _email:
851 user = cls.get_by_email(_email, case_insensitive=True)
850 user = cls.get_by_email(_email, case_insensitive=True)
852 if user:
851 if user:
853 return user
852 return user
854 # Maybe we can match by username?
853 # Maybe we can match by username?
855 _author = author_name(author)
854 _author = author_name(author)
856 user = cls.get_by_username(_author, case_insensitive=True)
855 user = cls.get_by_username(_author, case_insensitive=True)
857 if user:
856 if user:
858 return user
857 return user
859
858
860 def update_userdata(self, **kwargs):
859 def update_userdata(self, **kwargs):
861 usr = self
860 usr = self
862 old = usr.user_data
861 old = usr.user_data
863 old.update(**kwargs)
862 old.update(**kwargs)
864 usr.user_data = old
863 usr.user_data = old
865 Session().add(usr)
864 Session().add(usr)
866 log.debug('updated userdata with ', kwargs)
865 log.debug('updated userdata with ', kwargs)
867
866
868 def update_lastlogin(self):
867 def update_lastlogin(self):
869 """Update user lastlogin"""
868 """Update user lastlogin"""
870 self.last_login = datetime.datetime.now()
869 self.last_login = datetime.datetime.now()
871 Session().add(self)
870 Session().add(self)
872 log.debug('updated user %s lastlogin', self.username)
871 log.debug('updated user %s lastlogin', self.username)
873
872
874 def update_lastactivity(self):
873 def update_lastactivity(self):
875 """Update user lastactivity"""
874 """Update user lastactivity"""
876 self.last_activity = datetime.datetime.now()
875 self.last_activity = datetime.datetime.now()
877 Session().add(self)
876 Session().add(self)
878 log.debug('updated user %s lastactivity', self.username)
877 log.debug('updated user %s lastactivity', self.username)
879
878
880 def update_password(self, new_password):
879 def update_password(self, new_password):
881 from rhodecode.lib.auth import get_crypt_password
880 from rhodecode.lib.auth import get_crypt_password
882
881
883 self.password = get_crypt_password(new_password)
882 self.password = get_crypt_password(new_password)
884 Session().add(self)
883 Session().add(self)
885
884
886 @classmethod
885 @classmethod
887 def get_first_super_admin(cls):
886 def get_first_super_admin(cls):
888 user = User.query().filter(User.admin == true()).first()
887 user = User.query().filter(User.admin == true()).first()
889 if user is None:
888 if user is None:
890 raise Exception('FATAL: Missing administrative account!')
889 raise Exception('FATAL: Missing administrative account!')
891 return user
890 return user
892
891
893 @classmethod
892 @classmethod
894 def get_all_super_admins(cls):
893 def get_all_super_admins(cls):
895 """
894 """
896 Returns all admin accounts sorted by username
895 Returns all admin accounts sorted by username
897 """
896 """
898 return User.query().filter(User.admin == true())\
897 return User.query().filter(User.admin == true())\
899 .order_by(User.username.asc()).all()
898 .order_by(User.username.asc()).all()
900
899
901 @classmethod
900 @classmethod
902 def get_default_user(cls, cache=False, refresh=False):
901 def get_default_user(cls, cache=False, refresh=False):
903 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
902 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
904 if user is None:
903 if user is None:
905 raise Exception('FATAL: Missing default account!')
904 raise Exception('FATAL: Missing default account!')
906 if refresh:
905 if refresh:
907 # The default user might be based on outdated state which
906 # The default user might be based on outdated state which
908 # has been loaded from the cache.
907 # has been loaded from the cache.
909 # A call to refresh() ensures that the
908 # A call to refresh() ensures that the
910 # latest state from the database is used.
909 # latest state from the database is used.
911 Session().refresh(user)
910 Session().refresh(user)
912 return user
911 return user
913
912
914 def _get_default_perms(self, user, suffix=''):
913 def _get_default_perms(self, user, suffix=''):
915 from rhodecode.model.permission import PermissionModel
914 from rhodecode.model.permission import PermissionModel
916 return PermissionModel().get_default_perms(user.user_perms, suffix)
915 return PermissionModel().get_default_perms(user.user_perms, suffix)
917
916
918 def get_default_perms(self, suffix=''):
917 def get_default_perms(self, suffix=''):
919 return self._get_default_perms(self, suffix)
918 return self._get_default_perms(self, suffix)
920
919
921 def get_api_data(self, include_secrets=False, details='full'):
920 def get_api_data(self, include_secrets=False, details='full'):
922 """
921 """
923 Common function for generating user related data for API
922 Common function for generating user related data for API
924
923
925 :param include_secrets: By default secrets in the API data will be replaced
924 :param include_secrets: By default secrets in the API data will be replaced
926 by a placeholder value to prevent exposing this data by accident. In case
925 by a placeholder value to prevent exposing this data by accident. In case
927 this data shall be exposed, set this flag to ``True``.
926 this data shall be exposed, set this flag to ``True``.
928
927
929 :param details: details can be 'basic|full' basic gives only a subset of
928 :param details: details can be 'basic|full' basic gives only a subset of
930 the available user information that includes user_id, name and emails.
929 the available user information that includes user_id, name and emails.
931 """
930 """
932 user = self
931 user = self
933 user_data = self.user_data
932 user_data = self.user_data
934 data = {
933 data = {
935 'user_id': user.user_id,
934 'user_id': user.user_id,
936 'username': user.username,
935 'username': user.username,
937 'firstname': user.name,
936 'firstname': user.name,
938 'lastname': user.lastname,
937 'lastname': user.lastname,
939 'email': user.email,
938 'email': user.email,
940 'emails': user.emails,
939 'emails': user.emails,
941 }
940 }
942 if details == 'basic':
941 if details == 'basic':
943 return data
942 return data
944
943
945 auth_token_length = 40
944 auth_token_length = 40
946 auth_token_replacement = '*' * auth_token_length
945 auth_token_replacement = '*' * auth_token_length
947
946
948 extras = {
947 extras = {
949 'auth_tokens': [auth_token_replacement],
948 'auth_tokens': [auth_token_replacement],
950 'active': user.active,
949 'active': user.active,
951 'admin': user.admin,
950 'admin': user.admin,
952 'extern_type': user.extern_type,
951 'extern_type': user.extern_type,
953 'extern_name': user.extern_name,
952 'extern_name': user.extern_name,
954 'last_login': user.last_login,
953 'last_login': user.last_login,
955 'last_activity': user.last_activity,
954 'last_activity': user.last_activity,
956 'ip_addresses': user.ip_addresses,
955 'ip_addresses': user.ip_addresses,
957 'language': user_data.get('language')
956 'language': user_data.get('language')
958 }
957 }
959 data.update(extras)
958 data.update(extras)
960
959
961 if include_secrets:
960 if include_secrets:
962 data['auth_tokens'] = user.auth_tokens
961 data['auth_tokens'] = user.auth_tokens
963 return data
962 return data
964
963
965 def __json__(self):
964 def __json__(self):
966 data = {
965 data = {
967 'full_name': self.full_name,
966 'full_name': self.full_name,
968 'full_name_or_username': self.full_name_or_username,
967 'full_name_or_username': self.full_name_or_username,
969 'short_contact': self.short_contact,
968 'short_contact': self.short_contact,
970 'full_contact': self.full_contact,
969 'full_contact': self.full_contact,
971 }
970 }
972 data.update(self.get_api_data())
971 data.update(self.get_api_data())
973 return data
972 return data
974
973
975
974
976 class UserApiKeys(Base, BaseModel):
975 class UserApiKeys(Base, BaseModel):
977 __tablename__ = 'user_api_keys'
976 __tablename__ = 'user_api_keys'
978 __table_args__ = (
977 __table_args__ = (
979 Index('uak_api_key_idx', 'api_key', unique=True),
978 Index('uak_api_key_idx', 'api_key', unique=True),
980 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
979 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
981 {'extend_existing': True, 'mysql_engine': 'InnoDB',
980 {'extend_existing': True, 'mysql_engine': 'InnoDB',
982 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
981 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
983 )
982 )
984 __mapper_args__ = {}
983 __mapper_args__ = {}
985
984
986 # ApiKey role
985 # ApiKey role
987 ROLE_ALL = 'token_role_all'
986 ROLE_ALL = 'token_role_all'
988 ROLE_HTTP = 'token_role_http'
987 ROLE_HTTP = 'token_role_http'
989 ROLE_VCS = 'token_role_vcs'
988 ROLE_VCS = 'token_role_vcs'
990 ROLE_API = 'token_role_api'
989 ROLE_API = 'token_role_api'
991 ROLE_FEED = 'token_role_feed'
990 ROLE_FEED = 'token_role_feed'
992 ROLE_PASSWORD_RESET = 'token_password_reset'
991 ROLE_PASSWORD_RESET = 'token_password_reset'
993
992
994 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
993 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
995
994
996 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
995 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
997 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
996 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
998 api_key = Column("api_key", String(255), nullable=False, unique=True)
997 api_key = Column("api_key", String(255), nullable=False, unique=True)
999 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
998 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1000 expires = Column('expires', Float(53), nullable=False)
999 expires = Column('expires', Float(53), nullable=False)
1001 role = Column('role', String(255), nullable=True)
1000 role = Column('role', String(255), nullable=True)
1002 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1001 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1003
1002
1004 # scope columns
1003 # scope columns
1005 repo_id = Column(
1004 repo_id = Column(
1006 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1005 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1007 nullable=True, unique=None, default=None)
1006 nullable=True, unique=None, default=None)
1008 repo = relationship('Repository', lazy='joined')
1007 repo = relationship('Repository', lazy='joined')
1009
1008
1010 repo_group_id = Column(
1009 repo_group_id = Column(
1011 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1010 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1012 nullable=True, unique=None, default=None)
1011 nullable=True, unique=None, default=None)
1013 repo_group = relationship('RepoGroup', lazy='joined')
1012 repo_group = relationship('RepoGroup', lazy='joined')
1014
1013
1015 user = relationship('User', lazy='joined')
1014 user = relationship('User', lazy='joined')
1016
1015
1017 def __unicode__(self):
1016 def __unicode__(self):
1018 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1017 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1019
1018
1020 def __json__(self):
1019 def __json__(self):
1021 data = {
1020 data = {
1022 'auth_token': self.api_key,
1021 'auth_token': self.api_key,
1023 'role': self.role,
1022 'role': self.role,
1024 'scope': self.scope_humanized,
1023 'scope': self.scope_humanized,
1025 'expired': self.expired
1024 'expired': self.expired
1026 }
1025 }
1027 return data
1026 return data
1028
1027
1029 def get_api_data(self, include_secrets=False):
1028 def get_api_data(self, include_secrets=False):
1030 data = self.__json__()
1029 data = self.__json__()
1031 if include_secrets:
1030 if include_secrets:
1032 return data
1031 return data
1033 else:
1032 else:
1034 data['auth_token'] = self.token_obfuscated
1033 data['auth_token'] = self.token_obfuscated
1035 return data
1034 return data
1036
1035
1037 @hybrid_property
1036 @hybrid_property
1038 def description_safe(self):
1037 def description_safe(self):
1039 from rhodecode.lib import helpers as h
1038 from rhodecode.lib import helpers as h
1040 return h.escape(self.description)
1039 return h.escape(self.description)
1041
1040
1042 @property
1041 @property
1043 def expired(self):
1042 def expired(self):
1044 if self.expires == -1:
1043 if self.expires == -1:
1045 return False
1044 return False
1046 return time.time() > self.expires
1045 return time.time() > self.expires
1047
1046
1048 @classmethod
1047 @classmethod
1049 def _get_role_name(cls, role):
1048 def _get_role_name(cls, role):
1050 return {
1049 return {
1051 cls.ROLE_ALL: _('all'),
1050 cls.ROLE_ALL: _('all'),
1052 cls.ROLE_HTTP: _('http/web interface'),
1051 cls.ROLE_HTTP: _('http/web interface'),
1053 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1052 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1054 cls.ROLE_API: _('api calls'),
1053 cls.ROLE_API: _('api calls'),
1055 cls.ROLE_FEED: _('feed access'),
1054 cls.ROLE_FEED: _('feed access'),
1056 }.get(role, role)
1055 }.get(role, role)
1057
1056
1058 @property
1057 @property
1059 def role_humanized(self):
1058 def role_humanized(self):
1060 return self._get_role_name(self.role)
1059 return self._get_role_name(self.role)
1061
1060
1062 def _get_scope(self):
1061 def _get_scope(self):
1063 if self.repo:
1062 if self.repo:
1064 return repr(self.repo)
1063 return repr(self.repo)
1065 if self.repo_group:
1064 if self.repo_group:
1066 return repr(self.repo_group) + ' (recursive)'
1065 return repr(self.repo_group) + ' (recursive)'
1067 return 'global'
1066 return 'global'
1068
1067
1069 @property
1068 @property
1070 def scope_humanized(self):
1069 def scope_humanized(self):
1071 return self._get_scope()
1070 return self._get_scope()
1072
1071
1073 @property
1072 @property
1074 def token_obfuscated(self):
1073 def token_obfuscated(self):
1075 if self.api_key:
1074 if self.api_key:
1076 return self.api_key[:4] + "****"
1075 return self.api_key[:4] + "****"
1077
1076
1078
1077
1079 class UserEmailMap(Base, BaseModel):
1078 class UserEmailMap(Base, BaseModel):
1080 __tablename__ = 'user_email_map'
1079 __tablename__ = 'user_email_map'
1081 __table_args__ = (
1080 __table_args__ = (
1082 Index('uem_email_idx', 'email'),
1081 Index('uem_email_idx', 'email'),
1083 UniqueConstraint('email'),
1082 UniqueConstraint('email'),
1084 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1083 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1085 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1084 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1086 )
1085 )
1087 __mapper_args__ = {}
1086 __mapper_args__ = {}
1088
1087
1089 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1088 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1090 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1089 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1091 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1090 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1092 user = relationship('User', lazy='joined')
1091 user = relationship('User', lazy='joined')
1093
1092
1094 @validates('_email')
1093 @validates('_email')
1095 def validate_email(self, key, email):
1094 def validate_email(self, key, email):
1096 # check if this email is not main one
1095 # check if this email is not main one
1097 main_email = Session().query(User).filter(User.email == email).scalar()
1096 main_email = Session().query(User).filter(User.email == email).scalar()
1098 if main_email is not None:
1097 if main_email is not None:
1099 raise AttributeError('email %s is present is user table' % email)
1098 raise AttributeError('email %s is present is user table' % email)
1100 return email
1099 return email
1101
1100
1102 @hybrid_property
1101 @hybrid_property
1103 def email(self):
1102 def email(self):
1104 return self._email
1103 return self._email
1105
1104
1106 @email.setter
1105 @email.setter
1107 def email(self, val):
1106 def email(self, val):
1108 self._email = val.lower() if val else None
1107 self._email = val.lower() if val else None
1109
1108
1110
1109
1111 class UserIpMap(Base, BaseModel):
1110 class UserIpMap(Base, BaseModel):
1112 __tablename__ = 'user_ip_map'
1111 __tablename__ = 'user_ip_map'
1113 __table_args__ = (
1112 __table_args__ = (
1114 UniqueConstraint('user_id', 'ip_addr'),
1113 UniqueConstraint('user_id', 'ip_addr'),
1115 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1114 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1116 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1115 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1117 )
1116 )
1118 __mapper_args__ = {}
1117 __mapper_args__ = {}
1119
1118
1120 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1119 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1121 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1120 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1122 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1121 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1123 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1122 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1124 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1123 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1125 user = relationship('User', lazy='joined')
1124 user = relationship('User', lazy='joined')
1126
1125
1127 @hybrid_property
1126 @hybrid_property
1128 def description_safe(self):
1127 def description_safe(self):
1129 from rhodecode.lib import helpers as h
1128 from rhodecode.lib import helpers as h
1130 return h.escape(self.description)
1129 return h.escape(self.description)
1131
1130
1132 @classmethod
1131 @classmethod
1133 def _get_ip_range(cls, ip_addr):
1132 def _get_ip_range(cls, ip_addr):
1134 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1133 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1135 return [str(net.network_address), str(net.broadcast_address)]
1134 return [str(net.network_address), str(net.broadcast_address)]
1136
1135
1137 def __json__(self):
1136 def __json__(self):
1138 return {
1137 return {
1139 'ip_addr': self.ip_addr,
1138 'ip_addr': self.ip_addr,
1140 'ip_range': self._get_ip_range(self.ip_addr),
1139 'ip_range': self._get_ip_range(self.ip_addr),
1141 }
1140 }
1142
1141
1143 def __unicode__(self):
1142 def __unicode__(self):
1144 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1143 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1145 self.user_id, self.ip_addr)
1144 self.user_id, self.ip_addr)
1146
1145
1147
1146
1148 class UserSshKeys(Base, BaseModel):
1147 class UserSshKeys(Base, BaseModel):
1149 __tablename__ = 'user_ssh_keys'
1148 __tablename__ = 'user_ssh_keys'
1150 __table_args__ = (
1149 __table_args__ = (
1151 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1150 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1152
1151
1153 UniqueConstraint('ssh_key_fingerprint'),
1152 UniqueConstraint('ssh_key_fingerprint'),
1154
1153
1155 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1154 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1156 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1155 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1157 )
1156 )
1158 __mapper_args__ = {}
1157 __mapper_args__ = {}
1159
1158
1160 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1159 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1161 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1160 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1162 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(1024), nullable=False, unique=None, default=None)
1161 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(1024), nullable=False, unique=None, default=None)
1163
1162
1164 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1163 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1165
1164
1166 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1165 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1167 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1166 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1168 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1167 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1169
1168
1170 user = relationship('User', lazy='joined')
1169 user = relationship('User', lazy='joined')
1171
1170
1172 def __json__(self):
1171 def __json__(self):
1173 data = {
1172 data = {
1174 'ssh_fingerprint': self.ssh_key_fingerprint,
1173 'ssh_fingerprint': self.ssh_key_fingerprint,
1175 'description': self.description,
1174 'description': self.description,
1176 'created_on': self.created_on
1175 'created_on': self.created_on
1177 }
1176 }
1178 return data
1177 return data
1179
1178
1180 def get_api_data(self):
1179 def get_api_data(self):
1181 data = self.__json__()
1180 data = self.__json__()
1182 return data
1181 return data
1183
1182
1184
1183
1185 class UserLog(Base, BaseModel):
1184 class UserLog(Base, BaseModel):
1186 __tablename__ = 'user_logs'
1185 __tablename__ = 'user_logs'
1187 __table_args__ = (
1186 __table_args__ = (
1188 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1187 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1189 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1188 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1190 )
1189 )
1191 VERSION_1 = 'v1'
1190 VERSION_1 = 'v1'
1192 VERSION_2 = 'v2'
1191 VERSION_2 = 'v2'
1193 VERSIONS = [VERSION_1, VERSION_2]
1192 VERSIONS = [VERSION_1, VERSION_2]
1194
1193
1195 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1194 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1196 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1195 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1197 username = Column("username", String(255), nullable=True, unique=None, default=None)
1196 username = Column("username", String(255), nullable=True, unique=None, default=None)
1198 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1197 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1199 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1198 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1200 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1199 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1201 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1200 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1202 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1201 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1203
1202
1204 version = Column("version", String(255), nullable=True, default=VERSION_1)
1203 version = Column("version", String(255), nullable=True, default=VERSION_1)
1205 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1204 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1206 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1205 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1207
1206
1208 def __unicode__(self):
1207 def __unicode__(self):
1209 return u"<%s('id:%s:%s')>" % (
1208 return u"<%s('id:%s:%s')>" % (
1210 self.__class__.__name__, self.repository_name, self.action)
1209 self.__class__.__name__, self.repository_name, self.action)
1211
1210
1212 def __json__(self):
1211 def __json__(self):
1213 return {
1212 return {
1214 'user_id': self.user_id,
1213 'user_id': self.user_id,
1215 'username': self.username,
1214 'username': self.username,
1216 'repository_id': self.repository_id,
1215 'repository_id': self.repository_id,
1217 'repository_name': self.repository_name,
1216 'repository_name': self.repository_name,
1218 'user_ip': self.user_ip,
1217 'user_ip': self.user_ip,
1219 'action_date': self.action_date,
1218 'action_date': self.action_date,
1220 'action': self.action,
1219 'action': self.action,
1221 }
1220 }
1222
1221
1223 @property
1222 @property
1224 def action_as_day(self):
1223 def action_as_day(self):
1225 return datetime.date(*self.action_date.timetuple()[:3])
1224 return datetime.date(*self.action_date.timetuple()[:3])
1226
1225
1227 user = relationship('User')
1226 user = relationship('User')
1228 repository = relationship('Repository', cascade='')
1227 repository = relationship('Repository', cascade='')
1229
1228
1230
1229
1231 class UserGroup(Base, BaseModel):
1230 class UserGroup(Base, BaseModel):
1232 __tablename__ = 'users_groups'
1231 __tablename__ = 'users_groups'
1233 __table_args__ = (
1232 __table_args__ = (
1234 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1233 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1235 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1234 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1236 )
1235 )
1237
1236
1238 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1237 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1239 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1238 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1240 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1239 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1241 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1240 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1242 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1241 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1243 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1242 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1244 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1243 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1245 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1244 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1246
1245
1247 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1246 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1248 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1247 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1249 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1248 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1250 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1249 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1251 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1250 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1252 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1251 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1253
1252
1254 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1253 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1255
1254
1256 @classmethod
1255 @classmethod
1257 def _load_group_data(cls, column):
1256 def _load_group_data(cls, column):
1258 if not column:
1257 if not column:
1259 return {}
1258 return {}
1260
1259
1261 try:
1260 try:
1262 return json.loads(column) or {}
1261 return json.loads(column) or {}
1263 except TypeError:
1262 except TypeError:
1264 return {}
1263 return {}
1265
1264
1266 @hybrid_property
1265 @hybrid_property
1267 def description_safe(self):
1266 def description_safe(self):
1268 from rhodecode.lib import helpers as h
1267 from rhodecode.lib import helpers as h
1269 return h.escape(self.description)
1268 return h.escape(self.description)
1270
1269
1271 @hybrid_property
1270 @hybrid_property
1272 def group_data(self):
1271 def group_data(self):
1273 return self._load_group_data(self._group_data)
1272 return self._load_group_data(self._group_data)
1274
1273
1275 @group_data.expression
1274 @group_data.expression
1276 def group_data(self, **kwargs):
1275 def group_data(self, **kwargs):
1277 return self._group_data
1276 return self._group_data
1278
1277
1279 @group_data.setter
1278 @group_data.setter
1280 def group_data(self, val):
1279 def group_data(self, val):
1281 try:
1280 try:
1282 self._group_data = json.dumps(val)
1281 self._group_data = json.dumps(val)
1283 except Exception:
1282 except Exception:
1284 log.error(traceback.format_exc())
1283 log.error(traceback.format_exc())
1285
1284
1286 def __unicode__(self):
1285 def __unicode__(self):
1287 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1286 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1288 self.users_group_id,
1287 self.users_group_id,
1289 self.users_group_name)
1288 self.users_group_name)
1290
1289
1291 @classmethod
1290 @classmethod
1292 def get_by_group_name(cls, group_name, cache=False,
1291 def get_by_group_name(cls, group_name, cache=False,
1293 case_insensitive=False):
1292 case_insensitive=False):
1294 if case_insensitive:
1293 if case_insensitive:
1295 q = cls.query().filter(func.lower(cls.users_group_name) ==
1294 q = cls.query().filter(func.lower(cls.users_group_name) ==
1296 func.lower(group_name))
1295 func.lower(group_name))
1297
1296
1298 else:
1297 else:
1299 q = cls.query().filter(cls.users_group_name == group_name)
1298 q = cls.query().filter(cls.users_group_name == group_name)
1300 if cache:
1299 if cache:
1301 q = q.options(
1300 q = q.options(
1302 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1301 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1303 return q.scalar()
1302 return q.scalar()
1304
1303
1305 @classmethod
1304 @classmethod
1306 def get(cls, user_group_id, cache=False):
1305 def get(cls, user_group_id, cache=False):
1307 user_group = cls.query()
1306 user_group = cls.query()
1308 if cache:
1307 if cache:
1309 user_group = user_group.options(
1308 user_group = user_group.options(
1310 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1309 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1311 return user_group.get(user_group_id)
1310 return user_group.get(user_group_id)
1312
1311
1313 def permissions(self, with_admins=True, with_owner=True):
1312 def permissions(self, with_admins=True, with_owner=True):
1314 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1313 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1315 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1314 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1316 joinedload(UserUserGroupToPerm.user),
1315 joinedload(UserUserGroupToPerm.user),
1317 joinedload(UserUserGroupToPerm.permission),)
1316 joinedload(UserUserGroupToPerm.permission),)
1318
1317
1319 # get owners and admins and permissions. We do a trick of re-writing
1318 # get owners and admins and permissions. We do a trick of re-writing
1320 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1319 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1321 # has a global reference and changing one object propagates to all
1320 # has a global reference and changing one object propagates to all
1322 # others. This means if admin is also an owner admin_row that change
1321 # others. This means if admin is also an owner admin_row that change
1323 # would propagate to both objects
1322 # would propagate to both objects
1324 perm_rows = []
1323 perm_rows = []
1325 for _usr in q.all():
1324 for _usr in q.all():
1326 usr = AttributeDict(_usr.user.get_dict())
1325 usr = AttributeDict(_usr.user.get_dict())
1327 usr.permission = _usr.permission.permission_name
1326 usr.permission = _usr.permission.permission_name
1328 perm_rows.append(usr)
1327 perm_rows.append(usr)
1329
1328
1330 # filter the perm rows by 'default' first and then sort them by
1329 # filter the perm rows by 'default' first and then sort them by
1331 # admin,write,read,none permissions sorted again alphabetically in
1330 # admin,write,read,none permissions sorted again alphabetically in
1332 # each group
1331 # each group
1333 perm_rows = sorted(perm_rows, key=display_sort)
1332 perm_rows = sorted(perm_rows, key=display_sort)
1334
1333
1335 _admin_perm = 'usergroup.admin'
1334 _admin_perm = 'usergroup.admin'
1336 owner_row = []
1335 owner_row = []
1337 if with_owner:
1336 if with_owner:
1338 usr = AttributeDict(self.user.get_dict())
1337 usr = AttributeDict(self.user.get_dict())
1339 usr.owner_row = True
1338 usr.owner_row = True
1340 usr.permission = _admin_perm
1339 usr.permission = _admin_perm
1341 owner_row.append(usr)
1340 owner_row.append(usr)
1342
1341
1343 super_admin_rows = []
1342 super_admin_rows = []
1344 if with_admins:
1343 if with_admins:
1345 for usr in User.get_all_super_admins():
1344 for usr in User.get_all_super_admins():
1346 # if this admin is also owner, don't double the record
1345 # if this admin is also owner, don't double the record
1347 if usr.user_id == owner_row[0].user_id:
1346 if usr.user_id == owner_row[0].user_id:
1348 owner_row[0].admin_row = True
1347 owner_row[0].admin_row = True
1349 else:
1348 else:
1350 usr = AttributeDict(usr.get_dict())
1349 usr = AttributeDict(usr.get_dict())
1351 usr.admin_row = True
1350 usr.admin_row = True
1352 usr.permission = _admin_perm
1351 usr.permission = _admin_perm
1353 super_admin_rows.append(usr)
1352 super_admin_rows.append(usr)
1354
1353
1355 return super_admin_rows + owner_row + perm_rows
1354 return super_admin_rows + owner_row + perm_rows
1356
1355
1357 def permission_user_groups(self):
1356 def permission_user_groups(self):
1358 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1357 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1359 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1358 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1360 joinedload(UserGroupUserGroupToPerm.target_user_group),
1359 joinedload(UserGroupUserGroupToPerm.target_user_group),
1361 joinedload(UserGroupUserGroupToPerm.permission),)
1360 joinedload(UserGroupUserGroupToPerm.permission),)
1362
1361
1363 perm_rows = []
1362 perm_rows = []
1364 for _user_group in q.all():
1363 for _user_group in q.all():
1365 usr = AttributeDict(_user_group.user_group.get_dict())
1364 usr = AttributeDict(_user_group.user_group.get_dict())
1366 usr.permission = _user_group.permission.permission_name
1365 usr.permission = _user_group.permission.permission_name
1367 perm_rows.append(usr)
1366 perm_rows.append(usr)
1368
1367
1369 return perm_rows
1368 return perm_rows
1370
1369
1371 def _get_default_perms(self, user_group, suffix=''):
1370 def _get_default_perms(self, user_group, suffix=''):
1372 from rhodecode.model.permission import PermissionModel
1371 from rhodecode.model.permission import PermissionModel
1373 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1372 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1374
1373
1375 def get_default_perms(self, suffix=''):
1374 def get_default_perms(self, suffix=''):
1376 return self._get_default_perms(self, suffix)
1375 return self._get_default_perms(self, suffix)
1377
1376
1378 def get_api_data(self, with_group_members=True, include_secrets=False):
1377 def get_api_data(self, with_group_members=True, include_secrets=False):
1379 """
1378 """
1380 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1379 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1381 basically forwarded.
1380 basically forwarded.
1382
1381
1383 """
1382 """
1384 user_group = self
1383 user_group = self
1385 data = {
1384 data = {
1386 'users_group_id': user_group.users_group_id,
1385 'users_group_id': user_group.users_group_id,
1387 'group_name': user_group.users_group_name,
1386 'group_name': user_group.users_group_name,
1388 'group_description': user_group.user_group_description,
1387 'group_description': user_group.user_group_description,
1389 'active': user_group.users_group_active,
1388 'active': user_group.users_group_active,
1390 'owner': user_group.user.username,
1389 'owner': user_group.user.username,
1391 'owner_email': user_group.user.email,
1390 'owner_email': user_group.user.email,
1392 }
1391 }
1393
1392
1394 if with_group_members:
1393 if with_group_members:
1395 users = []
1394 users = []
1396 for user in user_group.members:
1395 for user in user_group.members:
1397 user = user.user
1396 user = user.user
1398 users.append(user.get_api_data(include_secrets=include_secrets))
1397 users.append(user.get_api_data(include_secrets=include_secrets))
1399 data['users'] = users
1398 data['users'] = users
1400
1399
1401 return data
1400 return data
1402
1401
1403
1402
1404 class UserGroupMember(Base, BaseModel):
1403 class UserGroupMember(Base, BaseModel):
1405 __tablename__ = 'users_groups_members'
1404 __tablename__ = 'users_groups_members'
1406 __table_args__ = (
1405 __table_args__ = (
1407 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1406 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1408 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1407 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1409 )
1408 )
1410
1409
1411 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1410 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1412 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1411 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1413 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1412 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1414
1413
1415 user = relationship('User', lazy='joined')
1414 user = relationship('User', lazy='joined')
1416 users_group = relationship('UserGroup')
1415 users_group = relationship('UserGroup')
1417
1416
1418 def __init__(self, gr_id='', u_id=''):
1417 def __init__(self, gr_id='', u_id=''):
1419 self.users_group_id = gr_id
1418 self.users_group_id = gr_id
1420 self.user_id = u_id
1419 self.user_id = u_id
1421
1420
1422
1421
1423 class RepositoryField(Base, BaseModel):
1422 class RepositoryField(Base, BaseModel):
1424 __tablename__ = 'repositories_fields'
1423 __tablename__ = 'repositories_fields'
1425 __table_args__ = (
1424 __table_args__ = (
1426 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1425 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1427 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1426 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1428 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1427 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1429 )
1428 )
1430 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1429 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1431
1430
1432 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1431 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1433 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1432 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1434 field_key = Column("field_key", String(250))
1433 field_key = Column("field_key", String(250))
1435 field_label = Column("field_label", String(1024), nullable=False)
1434 field_label = Column("field_label", String(1024), nullable=False)
1436 field_value = Column("field_value", String(10000), nullable=False)
1435 field_value = Column("field_value", String(10000), nullable=False)
1437 field_desc = Column("field_desc", String(1024), nullable=False)
1436 field_desc = Column("field_desc", String(1024), nullable=False)
1438 field_type = Column("field_type", String(255), nullable=False, unique=None)
1437 field_type = Column("field_type", String(255), nullable=False, unique=None)
1439 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1438 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1440
1439
1441 repository = relationship('Repository')
1440 repository = relationship('Repository')
1442
1441
1443 @property
1442 @property
1444 def field_key_prefixed(self):
1443 def field_key_prefixed(self):
1445 return 'ex_%s' % self.field_key
1444 return 'ex_%s' % self.field_key
1446
1445
1447 @classmethod
1446 @classmethod
1448 def un_prefix_key(cls, key):
1447 def un_prefix_key(cls, key):
1449 if key.startswith(cls.PREFIX):
1448 if key.startswith(cls.PREFIX):
1450 return key[len(cls.PREFIX):]
1449 return key[len(cls.PREFIX):]
1451 return key
1450 return key
1452
1451
1453 @classmethod
1452 @classmethod
1454 def get_by_key_name(cls, key, repo):
1453 def get_by_key_name(cls, key, repo):
1455 row = cls.query()\
1454 row = cls.query()\
1456 .filter(cls.repository == repo)\
1455 .filter(cls.repository == repo)\
1457 .filter(cls.field_key == key).scalar()
1456 .filter(cls.field_key == key).scalar()
1458 return row
1457 return row
1459
1458
1460
1459
1461 class Repository(Base, BaseModel):
1460 class Repository(Base, BaseModel):
1462 __tablename__ = 'repositories'
1461 __tablename__ = 'repositories'
1463 __table_args__ = (
1462 __table_args__ = (
1464 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1463 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1465 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1464 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1466 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1465 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1467 )
1466 )
1468 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1467 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1469 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1468 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1470
1469
1471 STATE_CREATED = 'repo_state_created'
1470 STATE_CREATED = 'repo_state_created'
1472 STATE_PENDING = 'repo_state_pending'
1471 STATE_PENDING = 'repo_state_pending'
1473 STATE_ERROR = 'repo_state_error'
1472 STATE_ERROR = 'repo_state_error'
1474
1473
1475 LOCK_AUTOMATIC = 'lock_auto'
1474 LOCK_AUTOMATIC = 'lock_auto'
1476 LOCK_API = 'lock_api'
1475 LOCK_API = 'lock_api'
1477 LOCK_WEB = 'lock_web'
1476 LOCK_WEB = 'lock_web'
1478 LOCK_PULL = 'lock_pull'
1477 LOCK_PULL = 'lock_pull'
1479
1478
1480 NAME_SEP = URL_SEP
1479 NAME_SEP = URL_SEP
1481
1480
1482 repo_id = Column(
1481 repo_id = Column(
1483 "repo_id", Integer(), nullable=False, unique=True, default=None,
1482 "repo_id", Integer(), nullable=False, unique=True, default=None,
1484 primary_key=True)
1483 primary_key=True)
1485 _repo_name = Column(
1484 _repo_name = Column(
1486 "repo_name", Text(), nullable=False, default=None)
1485 "repo_name", Text(), nullable=False, default=None)
1487 _repo_name_hash = Column(
1486 _repo_name_hash = Column(
1488 "repo_name_hash", String(255), nullable=False, unique=True)
1487 "repo_name_hash", String(255), nullable=False, unique=True)
1489 repo_state = Column("repo_state", String(255), nullable=True)
1488 repo_state = Column("repo_state", String(255), nullable=True)
1490
1489
1491 clone_uri = Column(
1490 clone_uri = Column(
1492 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1491 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1493 default=None)
1492 default=None)
1494 repo_type = Column(
1493 repo_type = Column(
1495 "repo_type", String(255), nullable=False, unique=False, default=None)
1494 "repo_type", String(255), nullable=False, unique=False, default=None)
1496 user_id = Column(
1495 user_id = Column(
1497 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1496 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1498 unique=False, default=None)
1497 unique=False, default=None)
1499 private = Column(
1498 private = Column(
1500 "private", Boolean(), nullable=True, unique=None, default=None)
1499 "private", Boolean(), nullable=True, unique=None, default=None)
1501 enable_statistics = Column(
1500 enable_statistics = Column(
1502 "statistics", Boolean(), nullable=True, unique=None, default=True)
1501 "statistics", Boolean(), nullable=True, unique=None, default=True)
1503 enable_downloads = Column(
1502 enable_downloads = Column(
1504 "downloads", Boolean(), nullable=True, unique=None, default=True)
1503 "downloads", Boolean(), nullable=True, unique=None, default=True)
1505 description = Column(
1504 description = Column(
1506 "description", String(10000), nullable=True, unique=None, default=None)
1505 "description", String(10000), nullable=True, unique=None, default=None)
1507 created_on = Column(
1506 created_on = Column(
1508 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1507 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1509 default=datetime.datetime.now)
1508 default=datetime.datetime.now)
1510 updated_on = Column(
1509 updated_on = Column(
1511 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1510 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1512 default=datetime.datetime.now)
1511 default=datetime.datetime.now)
1513 _landing_revision = Column(
1512 _landing_revision = Column(
1514 "landing_revision", String(255), nullable=False, unique=False,
1513 "landing_revision", String(255), nullable=False, unique=False,
1515 default=None)
1514 default=None)
1516 enable_locking = Column(
1515 enable_locking = Column(
1517 "enable_locking", Boolean(), nullable=False, unique=None,
1516 "enable_locking", Boolean(), nullable=False, unique=None,
1518 default=False)
1517 default=False)
1519 _locked = Column(
1518 _locked = Column(
1520 "locked", String(255), nullable=True, unique=False, default=None)
1519 "locked", String(255), nullable=True, unique=False, default=None)
1521 _changeset_cache = Column(
1520 _changeset_cache = Column(
1522 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1521 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1523
1522
1524 fork_id = Column(
1523 fork_id = Column(
1525 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1524 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1526 nullable=True, unique=False, default=None)
1525 nullable=True, unique=False, default=None)
1527 group_id = Column(
1526 group_id = Column(
1528 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1527 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1529 unique=False, default=None)
1528 unique=False, default=None)
1530
1529
1531 user = relationship('User', lazy='joined')
1530 user = relationship('User', lazy='joined')
1532 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1531 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1533 group = relationship('RepoGroup', lazy='joined')
1532 group = relationship('RepoGroup', lazy='joined')
1534 repo_to_perm = relationship(
1533 repo_to_perm = relationship(
1535 'UserRepoToPerm', cascade='all',
1534 'UserRepoToPerm', cascade='all',
1536 order_by='UserRepoToPerm.repo_to_perm_id')
1535 order_by='UserRepoToPerm.repo_to_perm_id')
1537 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1536 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1538 stats = relationship('Statistics', cascade='all', uselist=False)
1537 stats = relationship('Statistics', cascade='all', uselist=False)
1539
1538
1540 followers = relationship(
1539 followers = relationship(
1541 'UserFollowing',
1540 'UserFollowing',
1542 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1541 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1543 cascade='all')
1542 cascade='all')
1544 extra_fields = relationship(
1543 extra_fields = relationship(
1545 'RepositoryField', cascade="all, delete, delete-orphan")
1544 'RepositoryField', cascade="all, delete, delete-orphan")
1546 logs = relationship('UserLog')
1545 logs = relationship('UserLog')
1547 comments = relationship(
1546 comments = relationship(
1548 'ChangesetComment', cascade="all, delete, delete-orphan")
1547 'ChangesetComment', cascade="all, delete, delete-orphan")
1549 pull_requests_source = relationship(
1548 pull_requests_source = relationship(
1550 'PullRequest',
1549 'PullRequest',
1551 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1550 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1552 cascade="all, delete, delete-orphan")
1551 cascade="all, delete, delete-orphan")
1553 pull_requests_target = relationship(
1552 pull_requests_target = relationship(
1554 'PullRequest',
1553 'PullRequest',
1555 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1554 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1556 cascade="all, delete, delete-orphan")
1555 cascade="all, delete, delete-orphan")
1557 ui = relationship('RepoRhodeCodeUi', cascade="all")
1556 ui = relationship('RepoRhodeCodeUi', cascade="all")
1558 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1557 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1559 integrations = relationship('Integration',
1558 integrations = relationship('Integration',
1560 cascade="all, delete, delete-orphan")
1559 cascade="all, delete, delete-orphan")
1561
1560
1562 def __unicode__(self):
1561 def __unicode__(self):
1563 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1562 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1564 safe_unicode(self.repo_name))
1563 safe_unicode(self.repo_name))
1565
1564
1566 @hybrid_property
1565 @hybrid_property
1567 def description_safe(self):
1566 def description_safe(self):
1568 from rhodecode.lib import helpers as h
1567 from rhodecode.lib import helpers as h
1569 return h.escape(self.description)
1568 return h.escape(self.description)
1570
1569
1571 @hybrid_property
1570 @hybrid_property
1572 def landing_rev(self):
1571 def landing_rev(self):
1573 # always should return [rev_type, rev]
1572 # always should return [rev_type, rev]
1574 if self._landing_revision:
1573 if self._landing_revision:
1575 _rev_info = self._landing_revision.split(':')
1574 _rev_info = self._landing_revision.split(':')
1576 if len(_rev_info) < 2:
1575 if len(_rev_info) < 2:
1577 _rev_info.insert(0, 'rev')
1576 _rev_info.insert(0, 'rev')
1578 return [_rev_info[0], _rev_info[1]]
1577 return [_rev_info[0], _rev_info[1]]
1579 return [None, None]
1578 return [None, None]
1580
1579
1581 @landing_rev.setter
1580 @landing_rev.setter
1582 def landing_rev(self, val):
1581 def landing_rev(self, val):
1583 if ':' not in val:
1582 if ':' not in val:
1584 raise ValueError('value must be delimited with `:` and consist '
1583 raise ValueError('value must be delimited with `:` and consist '
1585 'of <rev_type>:<rev>, got %s instead' % val)
1584 'of <rev_type>:<rev>, got %s instead' % val)
1586 self._landing_revision = val
1585 self._landing_revision = val
1587
1586
1588 @hybrid_property
1587 @hybrid_property
1589 def locked(self):
1588 def locked(self):
1590 if self._locked:
1589 if self._locked:
1591 user_id, timelocked, reason = self._locked.split(':')
1590 user_id, timelocked, reason = self._locked.split(':')
1592 lock_values = int(user_id), timelocked, reason
1591 lock_values = int(user_id), timelocked, reason
1593 else:
1592 else:
1594 lock_values = [None, None, None]
1593 lock_values = [None, None, None]
1595 return lock_values
1594 return lock_values
1596
1595
1597 @locked.setter
1596 @locked.setter
1598 def locked(self, val):
1597 def locked(self, val):
1599 if val and isinstance(val, (list, tuple)):
1598 if val and isinstance(val, (list, tuple)):
1600 self._locked = ':'.join(map(str, val))
1599 self._locked = ':'.join(map(str, val))
1601 else:
1600 else:
1602 self._locked = None
1601 self._locked = None
1603
1602
1604 @hybrid_property
1603 @hybrid_property
1605 def changeset_cache(self):
1604 def changeset_cache(self):
1606 from rhodecode.lib.vcs.backends.base import EmptyCommit
1605 from rhodecode.lib.vcs.backends.base import EmptyCommit
1607 dummy = EmptyCommit().__json__()
1606 dummy = EmptyCommit().__json__()
1608 if not self._changeset_cache:
1607 if not self._changeset_cache:
1609 return dummy
1608 return dummy
1610 try:
1609 try:
1611 return json.loads(self._changeset_cache)
1610 return json.loads(self._changeset_cache)
1612 except TypeError:
1611 except TypeError:
1613 return dummy
1612 return dummy
1614 except Exception:
1613 except Exception:
1615 log.error(traceback.format_exc())
1614 log.error(traceback.format_exc())
1616 return dummy
1615 return dummy
1617
1616
1618 @changeset_cache.setter
1617 @changeset_cache.setter
1619 def changeset_cache(self, val):
1618 def changeset_cache(self, val):
1620 try:
1619 try:
1621 self._changeset_cache = json.dumps(val)
1620 self._changeset_cache = json.dumps(val)
1622 except Exception:
1621 except Exception:
1623 log.error(traceback.format_exc())
1622 log.error(traceback.format_exc())
1624
1623
1625 @hybrid_property
1624 @hybrid_property
1626 def repo_name(self):
1625 def repo_name(self):
1627 return self._repo_name
1626 return self._repo_name
1628
1627
1629 @repo_name.setter
1628 @repo_name.setter
1630 def repo_name(self, value):
1629 def repo_name(self, value):
1631 self._repo_name = value
1630 self._repo_name = value
1632 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1631 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1633
1632
1634 @classmethod
1633 @classmethod
1635 def normalize_repo_name(cls, repo_name):
1634 def normalize_repo_name(cls, repo_name):
1636 """
1635 """
1637 Normalizes os specific repo_name to the format internally stored inside
1636 Normalizes os specific repo_name to the format internally stored inside
1638 database using URL_SEP
1637 database using URL_SEP
1639
1638
1640 :param cls:
1639 :param cls:
1641 :param repo_name:
1640 :param repo_name:
1642 """
1641 """
1643 return cls.NAME_SEP.join(repo_name.split(os.sep))
1642 return cls.NAME_SEP.join(repo_name.split(os.sep))
1644
1643
1645 @classmethod
1644 @classmethod
1646 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1645 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1647 session = Session()
1646 session = Session()
1648 q = session.query(cls).filter(cls.repo_name == repo_name)
1647 q = session.query(cls).filter(cls.repo_name == repo_name)
1649
1648
1650 if cache:
1649 if cache:
1651 if identity_cache:
1650 if identity_cache:
1652 val = cls.identity_cache(session, 'repo_name', repo_name)
1651 val = cls.identity_cache(session, 'repo_name', repo_name)
1653 if val:
1652 if val:
1654 return val
1653 return val
1655 else:
1654 else:
1656 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1655 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1657 q = q.options(
1656 q = q.options(
1658 FromCache("sql_cache_short", cache_key))
1657 FromCache("sql_cache_short", cache_key))
1659
1658
1660 return q.scalar()
1659 return q.scalar()
1661
1660
1662 @classmethod
1661 @classmethod
1663 def get_by_full_path(cls, repo_full_path):
1662 def get_by_full_path(cls, repo_full_path):
1664 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1663 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1665 repo_name = cls.normalize_repo_name(repo_name)
1664 repo_name = cls.normalize_repo_name(repo_name)
1666 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1665 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1667
1666
1668 @classmethod
1667 @classmethod
1669 def get_repo_forks(cls, repo_id):
1668 def get_repo_forks(cls, repo_id):
1670 return cls.query().filter(Repository.fork_id == repo_id)
1669 return cls.query().filter(Repository.fork_id == repo_id)
1671
1670
1672 @classmethod
1671 @classmethod
1673 def base_path(cls):
1672 def base_path(cls):
1674 """
1673 """
1675 Returns base path when all repos are stored
1674 Returns base path when all repos are stored
1676
1675
1677 :param cls:
1676 :param cls:
1678 """
1677 """
1679 q = Session().query(RhodeCodeUi)\
1678 q = Session().query(RhodeCodeUi)\
1680 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1679 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1681 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1680 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1682 return q.one().ui_value
1681 return q.one().ui_value
1683
1682
1684 @classmethod
1683 @classmethod
1685 def is_valid(cls, repo_name):
1684 def is_valid(cls, repo_name):
1686 """
1685 """
1687 returns True if given repo name is a valid filesystem repository
1686 returns True if given repo name is a valid filesystem repository
1688
1687
1689 :param cls:
1688 :param cls:
1690 :param repo_name:
1689 :param repo_name:
1691 """
1690 """
1692 from rhodecode.lib.utils import is_valid_repo
1691 from rhodecode.lib.utils import is_valid_repo
1693
1692
1694 return is_valid_repo(repo_name, cls.base_path())
1693 return is_valid_repo(repo_name, cls.base_path())
1695
1694
1696 @classmethod
1695 @classmethod
1697 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1696 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1698 case_insensitive=True):
1697 case_insensitive=True):
1699 q = Repository.query()
1698 q = Repository.query()
1700
1699
1701 if not isinstance(user_id, Optional):
1700 if not isinstance(user_id, Optional):
1702 q = q.filter(Repository.user_id == user_id)
1701 q = q.filter(Repository.user_id == user_id)
1703
1702
1704 if not isinstance(group_id, Optional):
1703 if not isinstance(group_id, Optional):
1705 q = q.filter(Repository.group_id == group_id)
1704 q = q.filter(Repository.group_id == group_id)
1706
1705
1707 if case_insensitive:
1706 if case_insensitive:
1708 q = q.order_by(func.lower(Repository.repo_name))
1707 q = q.order_by(func.lower(Repository.repo_name))
1709 else:
1708 else:
1710 q = q.order_by(Repository.repo_name)
1709 q = q.order_by(Repository.repo_name)
1711 return q.all()
1710 return q.all()
1712
1711
1713 @property
1712 @property
1714 def forks(self):
1713 def forks(self):
1715 """
1714 """
1716 Return forks of this repo
1715 Return forks of this repo
1717 """
1716 """
1718 return Repository.get_repo_forks(self.repo_id)
1717 return Repository.get_repo_forks(self.repo_id)
1719
1718
1720 @property
1719 @property
1721 def parent(self):
1720 def parent(self):
1722 """
1721 """
1723 Returns fork parent
1722 Returns fork parent
1724 """
1723 """
1725 return self.fork
1724 return self.fork
1726
1725
1727 @property
1726 @property
1728 def just_name(self):
1727 def just_name(self):
1729 return self.repo_name.split(self.NAME_SEP)[-1]
1728 return self.repo_name.split(self.NAME_SEP)[-1]
1730
1729
1731 @property
1730 @property
1732 def groups_with_parents(self):
1731 def groups_with_parents(self):
1733 groups = []
1732 groups = []
1734 if self.group is None:
1733 if self.group is None:
1735 return groups
1734 return groups
1736
1735
1737 cur_gr = self.group
1736 cur_gr = self.group
1738 groups.insert(0, cur_gr)
1737 groups.insert(0, cur_gr)
1739 while 1:
1738 while 1:
1740 gr = getattr(cur_gr, 'parent_group', None)
1739 gr = getattr(cur_gr, 'parent_group', None)
1741 cur_gr = cur_gr.parent_group
1740 cur_gr = cur_gr.parent_group
1742 if gr is None:
1741 if gr is None:
1743 break
1742 break
1744 groups.insert(0, gr)
1743 groups.insert(0, gr)
1745
1744
1746 return groups
1745 return groups
1747
1746
1748 @property
1747 @property
1749 def groups_and_repo(self):
1748 def groups_and_repo(self):
1750 return self.groups_with_parents, self
1749 return self.groups_with_parents, self
1751
1750
1752 @LazyProperty
1751 @LazyProperty
1753 def repo_path(self):
1752 def repo_path(self):
1754 """
1753 """
1755 Returns base full path for that repository means where it actually
1754 Returns base full path for that repository means where it actually
1756 exists on a filesystem
1755 exists on a filesystem
1757 """
1756 """
1758 q = Session().query(RhodeCodeUi).filter(
1757 q = Session().query(RhodeCodeUi).filter(
1759 RhodeCodeUi.ui_key == self.NAME_SEP)
1758 RhodeCodeUi.ui_key == self.NAME_SEP)
1760 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1759 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1761 return q.one().ui_value
1760 return q.one().ui_value
1762
1761
1763 @property
1762 @property
1764 def repo_full_path(self):
1763 def repo_full_path(self):
1765 p = [self.repo_path]
1764 p = [self.repo_path]
1766 # we need to split the name by / since this is how we store the
1765 # we need to split the name by / since this is how we store the
1767 # names in the database, but that eventually needs to be converted
1766 # names in the database, but that eventually needs to be converted
1768 # into a valid system path
1767 # into a valid system path
1769 p += self.repo_name.split(self.NAME_SEP)
1768 p += self.repo_name.split(self.NAME_SEP)
1770 return os.path.join(*map(safe_unicode, p))
1769 return os.path.join(*map(safe_unicode, p))
1771
1770
1772 @property
1771 @property
1773 def cache_keys(self):
1772 def cache_keys(self):
1774 """
1773 """
1775 Returns associated cache keys for that repo
1774 Returns associated cache keys for that repo
1776 """
1775 """
1777 return CacheKey.query()\
1776 return CacheKey.query()\
1778 .filter(CacheKey.cache_args == self.repo_name)\
1777 .filter(CacheKey.cache_args == self.repo_name)\
1779 .order_by(CacheKey.cache_key)\
1778 .order_by(CacheKey.cache_key)\
1780 .all()
1779 .all()
1781
1780
1782 def get_new_name(self, repo_name):
1781 def get_new_name(self, repo_name):
1783 """
1782 """
1784 returns new full repository name based on assigned group and new new
1783 returns new full repository name based on assigned group and new new
1785
1784
1786 :param group_name:
1785 :param group_name:
1787 """
1786 """
1788 path_prefix = self.group.full_path_splitted if self.group else []
1787 path_prefix = self.group.full_path_splitted if self.group else []
1789 return self.NAME_SEP.join(path_prefix + [repo_name])
1788 return self.NAME_SEP.join(path_prefix + [repo_name])
1790
1789
1791 @property
1790 @property
1792 def _config(self):
1791 def _config(self):
1793 """
1792 """
1794 Returns db based config object.
1793 Returns db based config object.
1795 """
1794 """
1796 from rhodecode.lib.utils import make_db_config
1795 from rhodecode.lib.utils import make_db_config
1797 return make_db_config(clear_session=False, repo=self)
1796 return make_db_config(clear_session=False, repo=self)
1798
1797
1799 def permissions(self, with_admins=True, with_owner=True):
1798 def permissions(self, with_admins=True, with_owner=True):
1800 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1799 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1801 q = q.options(joinedload(UserRepoToPerm.repository),
1800 q = q.options(joinedload(UserRepoToPerm.repository),
1802 joinedload(UserRepoToPerm.user),
1801 joinedload(UserRepoToPerm.user),
1803 joinedload(UserRepoToPerm.permission),)
1802 joinedload(UserRepoToPerm.permission),)
1804
1803
1805 # get owners and admins and permissions. We do a trick of re-writing
1804 # get owners and admins and permissions. We do a trick of re-writing
1806 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1805 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1807 # has a global reference and changing one object propagates to all
1806 # has a global reference and changing one object propagates to all
1808 # others. This means if admin is also an owner admin_row that change
1807 # others. This means if admin is also an owner admin_row that change
1809 # would propagate to both objects
1808 # would propagate to both objects
1810 perm_rows = []
1809 perm_rows = []
1811 for _usr in q.all():
1810 for _usr in q.all():
1812 usr = AttributeDict(_usr.user.get_dict())
1811 usr = AttributeDict(_usr.user.get_dict())
1813 usr.permission = _usr.permission.permission_name
1812 usr.permission = _usr.permission.permission_name
1814 perm_rows.append(usr)
1813 perm_rows.append(usr)
1815
1814
1816 # filter the perm rows by 'default' first and then sort them by
1815 # filter the perm rows by 'default' first and then sort them by
1817 # admin,write,read,none permissions sorted again alphabetically in
1816 # admin,write,read,none permissions sorted again alphabetically in
1818 # each group
1817 # each group
1819 perm_rows = sorted(perm_rows, key=display_sort)
1818 perm_rows = sorted(perm_rows, key=display_sort)
1820
1819
1821 _admin_perm = 'repository.admin'
1820 _admin_perm = 'repository.admin'
1822 owner_row = []
1821 owner_row = []
1823 if with_owner:
1822 if with_owner:
1824 usr = AttributeDict(self.user.get_dict())
1823 usr = AttributeDict(self.user.get_dict())
1825 usr.owner_row = True
1824 usr.owner_row = True
1826 usr.permission = _admin_perm
1825 usr.permission = _admin_perm
1827 owner_row.append(usr)
1826 owner_row.append(usr)
1828
1827
1829 super_admin_rows = []
1828 super_admin_rows = []
1830 if with_admins:
1829 if with_admins:
1831 for usr in User.get_all_super_admins():
1830 for usr in User.get_all_super_admins():
1832 # if this admin is also owner, don't double the record
1831 # if this admin is also owner, don't double the record
1833 if usr.user_id == owner_row[0].user_id:
1832 if usr.user_id == owner_row[0].user_id:
1834 owner_row[0].admin_row = True
1833 owner_row[0].admin_row = True
1835 else:
1834 else:
1836 usr = AttributeDict(usr.get_dict())
1835 usr = AttributeDict(usr.get_dict())
1837 usr.admin_row = True
1836 usr.admin_row = True
1838 usr.permission = _admin_perm
1837 usr.permission = _admin_perm
1839 super_admin_rows.append(usr)
1838 super_admin_rows.append(usr)
1840
1839
1841 return super_admin_rows + owner_row + perm_rows
1840 return super_admin_rows + owner_row + perm_rows
1842
1841
1843 def permission_user_groups(self):
1842 def permission_user_groups(self):
1844 q = UserGroupRepoToPerm.query().filter(
1843 q = UserGroupRepoToPerm.query().filter(
1845 UserGroupRepoToPerm.repository == self)
1844 UserGroupRepoToPerm.repository == self)
1846 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1845 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1847 joinedload(UserGroupRepoToPerm.users_group),
1846 joinedload(UserGroupRepoToPerm.users_group),
1848 joinedload(UserGroupRepoToPerm.permission),)
1847 joinedload(UserGroupRepoToPerm.permission),)
1849
1848
1850 perm_rows = []
1849 perm_rows = []
1851 for _user_group in q.all():
1850 for _user_group in q.all():
1852 usr = AttributeDict(_user_group.users_group.get_dict())
1851 usr = AttributeDict(_user_group.users_group.get_dict())
1853 usr.permission = _user_group.permission.permission_name
1852 usr.permission = _user_group.permission.permission_name
1854 perm_rows.append(usr)
1853 perm_rows.append(usr)
1855
1854
1856 return perm_rows
1855 return perm_rows
1857
1856
1858 def get_api_data(self, include_secrets=False):
1857 def get_api_data(self, include_secrets=False):
1859 """
1858 """
1860 Common function for generating repo api data
1859 Common function for generating repo api data
1861
1860
1862 :param include_secrets: See :meth:`User.get_api_data`.
1861 :param include_secrets: See :meth:`User.get_api_data`.
1863
1862
1864 """
1863 """
1865 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1864 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1866 # move this methods on models level.
1865 # move this methods on models level.
1867 from rhodecode.model.settings import SettingsModel
1866 from rhodecode.model.settings import SettingsModel
1868 from rhodecode.model.repo import RepoModel
1867 from rhodecode.model.repo import RepoModel
1869
1868
1870 repo = self
1869 repo = self
1871 _user_id, _time, _reason = self.locked
1870 _user_id, _time, _reason = self.locked
1872
1871
1873 data = {
1872 data = {
1874 'repo_id': repo.repo_id,
1873 'repo_id': repo.repo_id,
1875 'repo_name': repo.repo_name,
1874 'repo_name': repo.repo_name,
1876 'repo_type': repo.repo_type,
1875 'repo_type': repo.repo_type,
1877 'clone_uri': repo.clone_uri or '',
1876 'clone_uri': repo.clone_uri or '',
1878 'url': RepoModel().get_url(self),
1877 'url': RepoModel().get_url(self),
1879 'private': repo.private,
1878 'private': repo.private,
1880 'created_on': repo.created_on,
1879 'created_on': repo.created_on,
1881 'description': repo.description_safe,
1880 'description': repo.description_safe,
1882 'landing_rev': repo.landing_rev,
1881 'landing_rev': repo.landing_rev,
1883 'owner': repo.user.username,
1882 'owner': repo.user.username,
1884 'fork_of': repo.fork.repo_name if repo.fork else None,
1883 'fork_of': repo.fork.repo_name if repo.fork else None,
1885 'fork_of_id': repo.fork.repo_id if repo.fork else None,
1884 'fork_of_id': repo.fork.repo_id if repo.fork else None,
1886 'enable_statistics': repo.enable_statistics,
1885 'enable_statistics': repo.enable_statistics,
1887 'enable_locking': repo.enable_locking,
1886 'enable_locking': repo.enable_locking,
1888 'enable_downloads': repo.enable_downloads,
1887 'enable_downloads': repo.enable_downloads,
1889 'last_changeset': repo.changeset_cache,
1888 'last_changeset': repo.changeset_cache,
1890 'locked_by': User.get(_user_id).get_api_data(
1889 'locked_by': User.get(_user_id).get_api_data(
1891 include_secrets=include_secrets) if _user_id else None,
1890 include_secrets=include_secrets) if _user_id else None,
1892 'locked_date': time_to_datetime(_time) if _time else None,
1891 'locked_date': time_to_datetime(_time) if _time else None,
1893 'lock_reason': _reason if _reason else None,
1892 'lock_reason': _reason if _reason else None,
1894 }
1893 }
1895
1894
1896 # TODO: mikhail: should be per-repo settings here
1895 # TODO: mikhail: should be per-repo settings here
1897 rc_config = SettingsModel().get_all_settings()
1896 rc_config = SettingsModel().get_all_settings()
1898 repository_fields = str2bool(
1897 repository_fields = str2bool(
1899 rc_config.get('rhodecode_repository_fields'))
1898 rc_config.get('rhodecode_repository_fields'))
1900 if repository_fields:
1899 if repository_fields:
1901 for f in self.extra_fields:
1900 for f in self.extra_fields:
1902 data[f.field_key_prefixed] = f.field_value
1901 data[f.field_key_prefixed] = f.field_value
1903
1902
1904 return data
1903 return data
1905
1904
1906 @classmethod
1905 @classmethod
1907 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1906 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1908 if not lock_time:
1907 if not lock_time:
1909 lock_time = time.time()
1908 lock_time = time.time()
1910 if not lock_reason:
1909 if not lock_reason:
1911 lock_reason = cls.LOCK_AUTOMATIC
1910 lock_reason = cls.LOCK_AUTOMATIC
1912 repo.locked = [user_id, lock_time, lock_reason]
1911 repo.locked = [user_id, lock_time, lock_reason]
1913 Session().add(repo)
1912 Session().add(repo)
1914 Session().commit()
1913 Session().commit()
1915
1914
1916 @classmethod
1915 @classmethod
1917 def unlock(cls, repo):
1916 def unlock(cls, repo):
1918 repo.locked = None
1917 repo.locked = None
1919 Session().add(repo)
1918 Session().add(repo)
1920 Session().commit()
1919 Session().commit()
1921
1920
1922 @classmethod
1921 @classmethod
1923 def getlock(cls, repo):
1922 def getlock(cls, repo):
1924 return repo.locked
1923 return repo.locked
1925
1924
1926 def is_user_lock(self, user_id):
1925 def is_user_lock(self, user_id):
1927 if self.lock[0]:
1926 if self.lock[0]:
1928 lock_user_id = safe_int(self.lock[0])
1927 lock_user_id = safe_int(self.lock[0])
1929 user_id = safe_int(user_id)
1928 user_id = safe_int(user_id)
1930 # both are ints, and they are equal
1929 # both are ints, and they are equal
1931 return all([lock_user_id, user_id]) and lock_user_id == user_id
1930 return all([lock_user_id, user_id]) and lock_user_id == user_id
1932
1931
1933 return False
1932 return False
1934
1933
1935 def get_locking_state(self, action, user_id, only_when_enabled=True):
1934 def get_locking_state(self, action, user_id, only_when_enabled=True):
1936 """
1935 """
1937 Checks locking on this repository, if locking is enabled and lock is
1936 Checks locking on this repository, if locking is enabled and lock is
1938 present returns a tuple of make_lock, locked, locked_by.
1937 present returns a tuple of make_lock, locked, locked_by.
1939 make_lock can have 3 states None (do nothing) True, make lock
1938 make_lock can have 3 states None (do nothing) True, make lock
1940 False release lock, This value is later propagated to hooks, which
1939 False release lock, This value is later propagated to hooks, which
1941 do the locking. Think about this as signals passed to hooks what to do.
1940 do the locking. Think about this as signals passed to hooks what to do.
1942
1941
1943 """
1942 """
1944 # TODO: johbo: This is part of the business logic and should be moved
1943 # TODO: johbo: This is part of the business logic and should be moved
1945 # into the RepositoryModel.
1944 # into the RepositoryModel.
1946
1945
1947 if action not in ('push', 'pull'):
1946 if action not in ('push', 'pull'):
1948 raise ValueError("Invalid action value: %s" % repr(action))
1947 raise ValueError("Invalid action value: %s" % repr(action))
1949
1948
1950 # defines if locked error should be thrown to user
1949 # defines if locked error should be thrown to user
1951 currently_locked = False
1950 currently_locked = False
1952 # defines if new lock should be made, tri-state
1951 # defines if new lock should be made, tri-state
1953 make_lock = None
1952 make_lock = None
1954 repo = self
1953 repo = self
1955 user = User.get(user_id)
1954 user = User.get(user_id)
1956
1955
1957 lock_info = repo.locked
1956 lock_info = repo.locked
1958
1957
1959 if repo and (repo.enable_locking or not only_when_enabled):
1958 if repo and (repo.enable_locking or not only_when_enabled):
1960 if action == 'push':
1959 if action == 'push':
1961 # check if it's already locked !, if it is compare users
1960 # check if it's already locked !, if it is compare users
1962 locked_by_user_id = lock_info[0]
1961 locked_by_user_id = lock_info[0]
1963 if user.user_id == locked_by_user_id:
1962 if user.user_id == locked_by_user_id:
1964 log.debug(
1963 log.debug(
1965 'Got `push` action from user %s, now unlocking', user)
1964 'Got `push` action from user %s, now unlocking', user)
1966 # unlock if we have push from user who locked
1965 # unlock if we have push from user who locked
1967 make_lock = False
1966 make_lock = False
1968 else:
1967 else:
1969 # we're not the same user who locked, ban with
1968 # we're not the same user who locked, ban with
1970 # code defined in settings (default is 423 HTTP Locked) !
1969 # code defined in settings (default is 423 HTTP Locked) !
1971 log.debug('Repo %s is currently locked by %s', repo, user)
1970 log.debug('Repo %s is currently locked by %s', repo, user)
1972 currently_locked = True
1971 currently_locked = True
1973 elif action == 'pull':
1972 elif action == 'pull':
1974 # [0] user [1] date
1973 # [0] user [1] date
1975 if lock_info[0] and lock_info[1]:
1974 if lock_info[0] and lock_info[1]:
1976 log.debug('Repo %s is currently locked by %s', repo, user)
1975 log.debug('Repo %s is currently locked by %s', repo, user)
1977 currently_locked = True
1976 currently_locked = True
1978 else:
1977 else:
1979 log.debug('Setting lock on repo %s by %s', repo, user)
1978 log.debug('Setting lock on repo %s by %s', repo, user)
1980 make_lock = True
1979 make_lock = True
1981
1980
1982 else:
1981 else:
1983 log.debug('Repository %s do not have locking enabled', repo)
1982 log.debug('Repository %s do not have locking enabled', repo)
1984
1983
1985 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1984 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1986 make_lock, currently_locked, lock_info)
1985 make_lock, currently_locked, lock_info)
1987
1986
1988 from rhodecode.lib.auth import HasRepoPermissionAny
1987 from rhodecode.lib.auth import HasRepoPermissionAny
1989 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1988 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1990 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1989 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1991 # if we don't have at least write permission we cannot make a lock
1990 # if we don't have at least write permission we cannot make a lock
1992 log.debug('lock state reset back to FALSE due to lack '
1991 log.debug('lock state reset back to FALSE due to lack '
1993 'of at least read permission')
1992 'of at least read permission')
1994 make_lock = False
1993 make_lock = False
1995
1994
1996 return make_lock, currently_locked, lock_info
1995 return make_lock, currently_locked, lock_info
1997
1996
1998 @property
1997 @property
1999 def last_db_change(self):
1998 def last_db_change(self):
2000 return self.updated_on
1999 return self.updated_on
2001
2000
2002 @property
2001 @property
2003 def clone_uri_hidden(self):
2002 def clone_uri_hidden(self):
2004 clone_uri = self.clone_uri
2003 clone_uri = self.clone_uri
2005 if clone_uri:
2004 if clone_uri:
2006 import urlobject
2005 import urlobject
2007 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2006 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2008 if url_obj.password:
2007 if url_obj.password:
2009 clone_uri = url_obj.with_password('*****')
2008 clone_uri = url_obj.with_password('*****')
2010 return clone_uri
2009 return clone_uri
2011
2010
2012 def clone_url(self, **override):
2011 def clone_url(self, **override):
2013 from rhodecode.model.settings import SettingsModel
2012 from rhodecode.model.settings import SettingsModel
2014
2013
2015 uri_tmpl = None
2014 uri_tmpl = None
2016 if 'with_id' in override:
2015 if 'with_id' in override:
2017 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2016 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2018 del override['with_id']
2017 del override['with_id']
2019
2018
2020 if 'uri_tmpl' in override:
2019 if 'uri_tmpl' in override:
2021 uri_tmpl = override['uri_tmpl']
2020 uri_tmpl = override['uri_tmpl']
2022 del override['uri_tmpl']
2021 del override['uri_tmpl']
2023
2022
2024 # we didn't override our tmpl from **overrides
2023 # we didn't override our tmpl from **overrides
2025 if not uri_tmpl:
2024 if not uri_tmpl:
2026 rc_config = SettingsModel().get_all_settings(cache=True)
2025 rc_config = SettingsModel().get_all_settings(cache=True)
2027 uri_tmpl = rc_config.get(
2026 uri_tmpl = rc_config.get(
2028 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2027 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2029
2028
2030 request = get_current_request()
2029 request = get_current_request()
2031 return get_clone_url(request=request,
2030 return get_clone_url(request=request,
2032 uri_tmpl=uri_tmpl,
2031 uri_tmpl=uri_tmpl,
2033 repo_name=self.repo_name,
2032 repo_name=self.repo_name,
2034 repo_id=self.repo_id, **override)
2033 repo_id=self.repo_id, **override)
2035
2034
2036 def set_state(self, state):
2035 def set_state(self, state):
2037 self.repo_state = state
2036 self.repo_state = state
2038 Session().add(self)
2037 Session().add(self)
2039 #==========================================================================
2038 #==========================================================================
2040 # SCM PROPERTIES
2039 # SCM PROPERTIES
2041 #==========================================================================
2040 #==========================================================================
2042
2041
2043 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
2042 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
2044 return get_commit_safe(
2043 return get_commit_safe(
2045 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
2044 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
2046
2045
2047 def get_changeset(self, rev=None, pre_load=None):
2046 def get_changeset(self, rev=None, pre_load=None):
2048 warnings.warn("Use get_commit", DeprecationWarning)
2047 warnings.warn("Use get_commit", DeprecationWarning)
2049 commit_id = None
2048 commit_id = None
2050 commit_idx = None
2049 commit_idx = None
2051 if isinstance(rev, basestring):
2050 if isinstance(rev, basestring):
2052 commit_id = rev
2051 commit_id = rev
2053 else:
2052 else:
2054 commit_idx = rev
2053 commit_idx = rev
2055 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2054 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2056 pre_load=pre_load)
2055 pre_load=pre_load)
2057
2056
2058 def get_landing_commit(self):
2057 def get_landing_commit(self):
2059 """
2058 """
2060 Returns landing commit, or if that doesn't exist returns the tip
2059 Returns landing commit, or if that doesn't exist returns the tip
2061 """
2060 """
2062 _rev_type, _rev = self.landing_rev
2061 _rev_type, _rev = self.landing_rev
2063 commit = self.get_commit(_rev)
2062 commit = self.get_commit(_rev)
2064 if isinstance(commit, EmptyCommit):
2063 if isinstance(commit, EmptyCommit):
2065 return self.get_commit()
2064 return self.get_commit()
2066 return commit
2065 return commit
2067
2066
2068 def update_commit_cache(self, cs_cache=None, config=None):
2067 def update_commit_cache(self, cs_cache=None, config=None):
2069 """
2068 """
2070 Update cache of last changeset for repository, keys should be::
2069 Update cache of last changeset for repository, keys should be::
2071
2070
2072 short_id
2071 short_id
2073 raw_id
2072 raw_id
2074 revision
2073 revision
2075 parents
2074 parents
2076 message
2075 message
2077 date
2076 date
2078 author
2077 author
2079
2078
2080 :param cs_cache:
2079 :param cs_cache:
2081 """
2080 """
2082 from rhodecode.lib.vcs.backends.base import BaseChangeset
2081 from rhodecode.lib.vcs.backends.base import BaseChangeset
2083 if cs_cache is None:
2082 if cs_cache is None:
2084 # use no-cache version here
2083 # use no-cache version here
2085 scm_repo = self.scm_instance(cache=False, config=config)
2084 scm_repo = self.scm_instance(cache=False, config=config)
2086 if scm_repo:
2085 if scm_repo:
2087 cs_cache = scm_repo.get_commit(
2086 cs_cache = scm_repo.get_commit(
2088 pre_load=["author", "date", "message", "parents"])
2087 pre_load=["author", "date", "message", "parents"])
2089 else:
2088 else:
2090 cs_cache = EmptyCommit()
2089 cs_cache = EmptyCommit()
2091
2090
2092 if isinstance(cs_cache, BaseChangeset):
2091 if isinstance(cs_cache, BaseChangeset):
2093 cs_cache = cs_cache.__json__()
2092 cs_cache = cs_cache.__json__()
2094
2093
2095 def is_outdated(new_cs_cache):
2094 def is_outdated(new_cs_cache):
2096 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2095 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2097 new_cs_cache['revision'] != self.changeset_cache['revision']):
2096 new_cs_cache['revision'] != self.changeset_cache['revision']):
2098 return True
2097 return True
2099 return False
2098 return False
2100
2099
2101 # check if we have maybe already latest cached revision
2100 # check if we have maybe already latest cached revision
2102 if is_outdated(cs_cache) or not self.changeset_cache:
2101 if is_outdated(cs_cache) or not self.changeset_cache:
2103 _default = datetime.datetime.fromtimestamp(0)
2102 _default = datetime.datetime.fromtimestamp(0)
2104 last_change = cs_cache.get('date') or _default
2103 last_change = cs_cache.get('date') or _default
2105 log.debug('updated repo %s with new cs cache %s',
2104 log.debug('updated repo %s with new cs cache %s',
2106 self.repo_name, cs_cache)
2105 self.repo_name, cs_cache)
2107 self.updated_on = last_change
2106 self.updated_on = last_change
2108 self.changeset_cache = cs_cache
2107 self.changeset_cache = cs_cache
2109 Session().add(self)
2108 Session().add(self)
2110 Session().commit()
2109 Session().commit()
2111 else:
2110 else:
2112 log.debug('Skipping update_commit_cache for repo:`%s` '
2111 log.debug('Skipping update_commit_cache for repo:`%s` '
2113 'commit already with latest changes', self.repo_name)
2112 'commit already with latest changes', self.repo_name)
2114
2113
2115 @property
2114 @property
2116 def tip(self):
2115 def tip(self):
2117 return self.get_commit('tip')
2116 return self.get_commit('tip')
2118
2117
2119 @property
2118 @property
2120 def author(self):
2119 def author(self):
2121 return self.tip.author
2120 return self.tip.author
2122
2121
2123 @property
2122 @property
2124 def last_change(self):
2123 def last_change(self):
2125 return self.scm_instance().last_change
2124 return self.scm_instance().last_change
2126
2125
2127 def get_comments(self, revisions=None):
2126 def get_comments(self, revisions=None):
2128 """
2127 """
2129 Returns comments for this repository grouped by revisions
2128 Returns comments for this repository grouped by revisions
2130
2129
2131 :param revisions: filter query by revisions only
2130 :param revisions: filter query by revisions only
2132 """
2131 """
2133 cmts = ChangesetComment.query()\
2132 cmts = ChangesetComment.query()\
2134 .filter(ChangesetComment.repo == self)
2133 .filter(ChangesetComment.repo == self)
2135 if revisions:
2134 if revisions:
2136 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2135 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2137 grouped = collections.defaultdict(list)
2136 grouped = collections.defaultdict(list)
2138 for cmt in cmts.all():
2137 for cmt in cmts.all():
2139 grouped[cmt.revision].append(cmt)
2138 grouped[cmt.revision].append(cmt)
2140 return grouped
2139 return grouped
2141
2140
2142 def statuses(self, revisions=None):
2141 def statuses(self, revisions=None):
2143 """
2142 """
2144 Returns statuses for this repository
2143 Returns statuses for this repository
2145
2144
2146 :param revisions: list of revisions to get statuses for
2145 :param revisions: list of revisions to get statuses for
2147 """
2146 """
2148 statuses = ChangesetStatus.query()\
2147 statuses = ChangesetStatus.query()\
2149 .filter(ChangesetStatus.repo == self)\
2148 .filter(ChangesetStatus.repo == self)\
2150 .filter(ChangesetStatus.version == 0)
2149 .filter(ChangesetStatus.version == 0)
2151
2150
2152 if revisions:
2151 if revisions:
2153 # Try doing the filtering in chunks to avoid hitting limits
2152 # Try doing the filtering in chunks to avoid hitting limits
2154 size = 500
2153 size = 500
2155 status_results = []
2154 status_results = []
2156 for chunk in xrange(0, len(revisions), size):
2155 for chunk in xrange(0, len(revisions), size):
2157 status_results += statuses.filter(
2156 status_results += statuses.filter(
2158 ChangesetStatus.revision.in_(
2157 ChangesetStatus.revision.in_(
2159 revisions[chunk: chunk+size])
2158 revisions[chunk: chunk+size])
2160 ).all()
2159 ).all()
2161 else:
2160 else:
2162 status_results = statuses.all()
2161 status_results = statuses.all()
2163
2162
2164 grouped = {}
2163 grouped = {}
2165
2164
2166 # maybe we have open new pullrequest without a status?
2165 # maybe we have open new pullrequest without a status?
2167 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2166 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2168 status_lbl = ChangesetStatus.get_status_lbl(stat)
2167 status_lbl = ChangesetStatus.get_status_lbl(stat)
2169 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2168 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2170 for rev in pr.revisions:
2169 for rev in pr.revisions:
2171 pr_id = pr.pull_request_id
2170 pr_id = pr.pull_request_id
2172 pr_repo = pr.target_repo.repo_name
2171 pr_repo = pr.target_repo.repo_name
2173 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2172 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2174
2173
2175 for stat in status_results:
2174 for stat in status_results:
2176 pr_id = pr_repo = None
2175 pr_id = pr_repo = None
2177 if stat.pull_request:
2176 if stat.pull_request:
2178 pr_id = stat.pull_request.pull_request_id
2177 pr_id = stat.pull_request.pull_request_id
2179 pr_repo = stat.pull_request.target_repo.repo_name
2178 pr_repo = stat.pull_request.target_repo.repo_name
2180 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2179 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2181 pr_id, pr_repo]
2180 pr_id, pr_repo]
2182 return grouped
2181 return grouped
2183
2182
2184 # ==========================================================================
2183 # ==========================================================================
2185 # SCM CACHE INSTANCE
2184 # SCM CACHE INSTANCE
2186 # ==========================================================================
2185 # ==========================================================================
2187
2186
2188 def scm_instance(self, **kwargs):
2187 def scm_instance(self, **kwargs):
2189 import rhodecode
2188 import rhodecode
2190
2189
2191 # Passing a config will not hit the cache currently only used
2190 # Passing a config will not hit the cache currently only used
2192 # for repo2dbmapper
2191 # for repo2dbmapper
2193 config = kwargs.pop('config', None)
2192 config = kwargs.pop('config', None)
2194 cache = kwargs.pop('cache', None)
2193 cache = kwargs.pop('cache', None)
2195 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2194 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2196 # if cache is NOT defined use default global, else we have a full
2195 # if cache is NOT defined use default global, else we have a full
2197 # control over cache behaviour
2196 # control over cache behaviour
2198 if cache is None and full_cache and not config:
2197 if cache is None and full_cache and not config:
2199 return self._get_instance_cached()
2198 return self._get_instance_cached()
2200 return self._get_instance(cache=bool(cache), config=config)
2199 return self._get_instance(cache=bool(cache), config=config)
2201
2200
2202 def _get_instance_cached(self):
2201 def _get_instance_cached(self):
2203 @cache_region('long_term')
2202 @cache_region('long_term')
2204 def _get_repo(cache_key):
2203 def _get_repo(cache_key):
2205 return self._get_instance()
2204 return self._get_instance()
2206
2205
2207 invalidator_context = CacheKey.repo_context_cache(
2206 invalidator_context = CacheKey.repo_context_cache(
2208 _get_repo, self.repo_name, None, thread_scoped=True)
2207 _get_repo, self.repo_name, None, thread_scoped=True)
2209
2208
2210 with invalidator_context as context:
2209 with invalidator_context as context:
2211 context.invalidate()
2210 context.invalidate()
2212 repo = context.compute()
2211 repo = context.compute()
2213
2212
2214 return repo
2213 return repo
2215
2214
2216 def _get_instance(self, cache=True, config=None):
2215 def _get_instance(self, cache=True, config=None):
2217 config = config or self._config
2216 config = config or self._config
2218 custom_wire = {
2217 custom_wire = {
2219 'cache': cache # controls the vcs.remote cache
2218 'cache': cache # controls the vcs.remote cache
2220 }
2219 }
2221 repo = get_vcs_instance(
2220 repo = get_vcs_instance(
2222 repo_path=safe_str(self.repo_full_path),
2221 repo_path=safe_str(self.repo_full_path),
2223 config=config,
2222 config=config,
2224 with_wire=custom_wire,
2223 with_wire=custom_wire,
2225 create=False,
2224 create=False,
2226 _vcs_alias=self.repo_type)
2225 _vcs_alias=self.repo_type)
2227
2226
2228 return repo
2227 return repo
2229
2228
2230 def __json__(self):
2229 def __json__(self):
2231 return {'landing_rev': self.landing_rev}
2230 return {'landing_rev': self.landing_rev}
2232
2231
2233 def get_dict(self):
2232 def get_dict(self):
2234
2233
2235 # Since we transformed `repo_name` to a hybrid property, we need to
2234 # Since we transformed `repo_name` to a hybrid property, we need to
2236 # keep compatibility with the code which uses `repo_name` field.
2235 # keep compatibility with the code which uses `repo_name` field.
2237
2236
2238 result = super(Repository, self).get_dict()
2237 result = super(Repository, self).get_dict()
2239 result['repo_name'] = result.pop('_repo_name', None)
2238 result['repo_name'] = result.pop('_repo_name', None)
2240 return result
2239 return result
2241
2240
2242
2241
2243 class RepoGroup(Base, BaseModel):
2242 class RepoGroup(Base, BaseModel):
2244 __tablename__ = 'groups'
2243 __tablename__ = 'groups'
2245 __table_args__ = (
2244 __table_args__ = (
2246 UniqueConstraint('group_name', 'group_parent_id'),
2245 UniqueConstraint('group_name', 'group_parent_id'),
2247 CheckConstraint('group_id != group_parent_id'),
2246 CheckConstraint('group_id != group_parent_id'),
2248 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2247 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2249 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2248 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2250 )
2249 )
2251 __mapper_args__ = {'order_by': 'group_name'}
2250 __mapper_args__ = {'order_by': 'group_name'}
2252
2251
2253 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2252 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2254
2253
2255 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2254 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2256 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2255 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2257 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2256 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2258 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2257 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2259 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2258 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2260 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2259 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2261 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2260 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2262 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2261 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2263 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2262 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2264
2263
2265 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2264 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2266 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2265 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2267 parent_group = relationship('RepoGroup', remote_side=group_id)
2266 parent_group = relationship('RepoGroup', remote_side=group_id)
2268 user = relationship('User')
2267 user = relationship('User')
2269 integrations = relationship('Integration',
2268 integrations = relationship('Integration',
2270 cascade="all, delete, delete-orphan")
2269 cascade="all, delete, delete-orphan")
2271
2270
2272 def __init__(self, group_name='', parent_group=None):
2271 def __init__(self, group_name='', parent_group=None):
2273 self.group_name = group_name
2272 self.group_name = group_name
2274 self.parent_group = parent_group
2273 self.parent_group = parent_group
2275
2274
2276 def __unicode__(self):
2275 def __unicode__(self):
2277 return u"<%s('id:%s:%s')>" % (
2276 return u"<%s('id:%s:%s')>" % (
2278 self.__class__.__name__, self.group_id, self.group_name)
2277 self.__class__.__name__, self.group_id, self.group_name)
2279
2278
2280 @hybrid_property
2279 @hybrid_property
2281 def description_safe(self):
2280 def description_safe(self):
2282 from rhodecode.lib import helpers as h
2281 from rhodecode.lib import helpers as h
2283 return h.escape(self.group_description)
2282 return h.escape(self.group_description)
2284
2283
2285 @classmethod
2284 @classmethod
2286 def _generate_choice(cls, repo_group):
2285 def _generate_choice(cls, repo_group):
2287 from webhelpers.html import literal as _literal
2286 from webhelpers.html import literal as _literal
2288 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2287 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2289 return repo_group.group_id, _name(repo_group.full_path_splitted)
2288 return repo_group.group_id, _name(repo_group.full_path_splitted)
2290
2289
2291 @classmethod
2290 @classmethod
2292 def groups_choices(cls, groups=None, show_empty_group=True):
2291 def groups_choices(cls, groups=None, show_empty_group=True):
2293 if not groups:
2292 if not groups:
2294 groups = cls.query().all()
2293 groups = cls.query().all()
2295
2294
2296 repo_groups = []
2295 repo_groups = []
2297 if show_empty_group:
2296 if show_empty_group:
2298 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2297 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2299
2298
2300 repo_groups.extend([cls._generate_choice(x) for x in groups])
2299 repo_groups.extend([cls._generate_choice(x) for x in groups])
2301
2300
2302 repo_groups = sorted(
2301 repo_groups = sorted(
2303 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2302 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2304 return repo_groups
2303 return repo_groups
2305
2304
2306 @classmethod
2305 @classmethod
2307 def url_sep(cls):
2306 def url_sep(cls):
2308 return URL_SEP
2307 return URL_SEP
2309
2308
2310 @classmethod
2309 @classmethod
2311 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2310 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2312 if case_insensitive:
2311 if case_insensitive:
2313 gr = cls.query().filter(func.lower(cls.group_name)
2312 gr = cls.query().filter(func.lower(cls.group_name)
2314 == func.lower(group_name))
2313 == func.lower(group_name))
2315 else:
2314 else:
2316 gr = cls.query().filter(cls.group_name == group_name)
2315 gr = cls.query().filter(cls.group_name == group_name)
2317 if cache:
2316 if cache:
2318 name_key = _hash_key(group_name)
2317 name_key = _hash_key(group_name)
2319 gr = gr.options(
2318 gr = gr.options(
2320 FromCache("sql_cache_short", "get_group_%s" % name_key))
2319 FromCache("sql_cache_short", "get_group_%s" % name_key))
2321 return gr.scalar()
2320 return gr.scalar()
2322
2321
2323 @classmethod
2322 @classmethod
2324 def get_user_personal_repo_group(cls, user_id):
2323 def get_user_personal_repo_group(cls, user_id):
2325 user = User.get(user_id)
2324 user = User.get(user_id)
2326 if user.username == User.DEFAULT_USER:
2325 if user.username == User.DEFAULT_USER:
2327 return None
2326 return None
2328
2327
2329 return cls.query()\
2328 return cls.query()\
2330 .filter(cls.personal == true()) \
2329 .filter(cls.personal == true()) \
2331 .filter(cls.user == user).scalar()
2330 .filter(cls.user == user).scalar()
2332
2331
2333 @classmethod
2332 @classmethod
2334 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2333 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2335 case_insensitive=True):
2334 case_insensitive=True):
2336 q = RepoGroup.query()
2335 q = RepoGroup.query()
2337
2336
2338 if not isinstance(user_id, Optional):
2337 if not isinstance(user_id, Optional):
2339 q = q.filter(RepoGroup.user_id == user_id)
2338 q = q.filter(RepoGroup.user_id == user_id)
2340
2339
2341 if not isinstance(group_id, Optional):
2340 if not isinstance(group_id, Optional):
2342 q = q.filter(RepoGroup.group_parent_id == group_id)
2341 q = q.filter(RepoGroup.group_parent_id == group_id)
2343
2342
2344 if case_insensitive:
2343 if case_insensitive:
2345 q = q.order_by(func.lower(RepoGroup.group_name))
2344 q = q.order_by(func.lower(RepoGroup.group_name))
2346 else:
2345 else:
2347 q = q.order_by(RepoGroup.group_name)
2346 q = q.order_by(RepoGroup.group_name)
2348 return q.all()
2347 return q.all()
2349
2348
2350 @property
2349 @property
2351 def parents(self):
2350 def parents(self):
2352 parents_recursion_limit = 10
2351 parents_recursion_limit = 10
2353 groups = []
2352 groups = []
2354 if self.parent_group is None:
2353 if self.parent_group is None:
2355 return groups
2354 return groups
2356 cur_gr = self.parent_group
2355 cur_gr = self.parent_group
2357 groups.insert(0, cur_gr)
2356 groups.insert(0, cur_gr)
2358 cnt = 0
2357 cnt = 0
2359 while 1:
2358 while 1:
2360 cnt += 1
2359 cnt += 1
2361 gr = getattr(cur_gr, 'parent_group', None)
2360 gr = getattr(cur_gr, 'parent_group', None)
2362 cur_gr = cur_gr.parent_group
2361 cur_gr = cur_gr.parent_group
2363 if gr is None:
2362 if gr is None:
2364 break
2363 break
2365 if cnt == parents_recursion_limit:
2364 if cnt == parents_recursion_limit:
2366 # this will prevent accidental infinit loops
2365 # this will prevent accidental infinit loops
2367 log.error(('more than %s parents found for group %s, stopping '
2366 log.error(('more than %s parents found for group %s, stopping '
2368 'recursive parent fetching' % (parents_recursion_limit, self)))
2367 'recursive parent fetching' % (parents_recursion_limit, self)))
2369 break
2368 break
2370
2369
2371 groups.insert(0, gr)
2370 groups.insert(0, gr)
2372 return groups
2371 return groups
2373
2372
2374 @property
2373 @property
2375 def last_db_change(self):
2374 def last_db_change(self):
2376 return self.updated_on
2375 return self.updated_on
2377
2376
2378 @property
2377 @property
2379 def children(self):
2378 def children(self):
2380 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2379 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2381
2380
2382 @property
2381 @property
2383 def name(self):
2382 def name(self):
2384 return self.group_name.split(RepoGroup.url_sep())[-1]
2383 return self.group_name.split(RepoGroup.url_sep())[-1]
2385
2384
2386 @property
2385 @property
2387 def full_path(self):
2386 def full_path(self):
2388 return self.group_name
2387 return self.group_name
2389
2388
2390 @property
2389 @property
2391 def full_path_splitted(self):
2390 def full_path_splitted(self):
2392 return self.group_name.split(RepoGroup.url_sep())
2391 return self.group_name.split(RepoGroup.url_sep())
2393
2392
2394 @property
2393 @property
2395 def repositories(self):
2394 def repositories(self):
2396 return Repository.query()\
2395 return Repository.query()\
2397 .filter(Repository.group == self)\
2396 .filter(Repository.group == self)\
2398 .order_by(Repository.repo_name)
2397 .order_by(Repository.repo_name)
2399
2398
2400 @property
2399 @property
2401 def repositories_recursive_count(self):
2400 def repositories_recursive_count(self):
2402 cnt = self.repositories.count()
2401 cnt = self.repositories.count()
2403
2402
2404 def children_count(group):
2403 def children_count(group):
2405 cnt = 0
2404 cnt = 0
2406 for child in group.children:
2405 for child in group.children:
2407 cnt += child.repositories.count()
2406 cnt += child.repositories.count()
2408 cnt += children_count(child)
2407 cnt += children_count(child)
2409 return cnt
2408 return cnt
2410
2409
2411 return cnt + children_count(self)
2410 return cnt + children_count(self)
2412
2411
2413 def _recursive_objects(self, include_repos=True):
2412 def _recursive_objects(self, include_repos=True):
2414 all_ = []
2413 all_ = []
2415
2414
2416 def _get_members(root_gr):
2415 def _get_members(root_gr):
2417 if include_repos:
2416 if include_repos:
2418 for r in root_gr.repositories:
2417 for r in root_gr.repositories:
2419 all_.append(r)
2418 all_.append(r)
2420 childs = root_gr.children.all()
2419 childs = root_gr.children.all()
2421 if childs:
2420 if childs:
2422 for gr in childs:
2421 for gr in childs:
2423 all_.append(gr)
2422 all_.append(gr)
2424 _get_members(gr)
2423 _get_members(gr)
2425
2424
2426 _get_members(self)
2425 _get_members(self)
2427 return [self] + all_
2426 return [self] + all_
2428
2427
2429 def recursive_groups_and_repos(self):
2428 def recursive_groups_and_repos(self):
2430 """
2429 """
2431 Recursive return all groups, with repositories in those groups
2430 Recursive return all groups, with repositories in those groups
2432 """
2431 """
2433 return self._recursive_objects()
2432 return self._recursive_objects()
2434
2433
2435 def recursive_groups(self):
2434 def recursive_groups(self):
2436 """
2435 """
2437 Returns all children groups for this group including children of children
2436 Returns all children groups for this group including children of children
2438 """
2437 """
2439 return self._recursive_objects(include_repos=False)
2438 return self._recursive_objects(include_repos=False)
2440
2439
2441 def get_new_name(self, group_name):
2440 def get_new_name(self, group_name):
2442 """
2441 """
2443 returns new full group name based on parent and new name
2442 returns new full group name based on parent and new name
2444
2443
2445 :param group_name:
2444 :param group_name:
2446 """
2445 """
2447 path_prefix = (self.parent_group.full_path_splitted if
2446 path_prefix = (self.parent_group.full_path_splitted if
2448 self.parent_group else [])
2447 self.parent_group else [])
2449 return RepoGroup.url_sep().join(path_prefix + [group_name])
2448 return RepoGroup.url_sep().join(path_prefix + [group_name])
2450
2449
2451 def permissions(self, with_admins=True, with_owner=True):
2450 def permissions(self, with_admins=True, with_owner=True):
2452 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2451 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2453 q = q.options(joinedload(UserRepoGroupToPerm.group),
2452 q = q.options(joinedload(UserRepoGroupToPerm.group),
2454 joinedload(UserRepoGroupToPerm.user),
2453 joinedload(UserRepoGroupToPerm.user),
2455 joinedload(UserRepoGroupToPerm.permission),)
2454 joinedload(UserRepoGroupToPerm.permission),)
2456
2455
2457 # get owners and admins and permissions. We do a trick of re-writing
2456 # get owners and admins and permissions. We do a trick of re-writing
2458 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2457 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2459 # has a global reference and changing one object propagates to all
2458 # has a global reference and changing one object propagates to all
2460 # others. This means if admin is also an owner admin_row that change
2459 # others. This means if admin is also an owner admin_row that change
2461 # would propagate to both objects
2460 # would propagate to both objects
2462 perm_rows = []
2461 perm_rows = []
2463 for _usr in q.all():
2462 for _usr in q.all():
2464 usr = AttributeDict(_usr.user.get_dict())
2463 usr = AttributeDict(_usr.user.get_dict())
2465 usr.permission = _usr.permission.permission_name
2464 usr.permission = _usr.permission.permission_name
2466 perm_rows.append(usr)
2465 perm_rows.append(usr)
2467
2466
2468 # filter the perm rows by 'default' first and then sort them by
2467 # filter the perm rows by 'default' first and then sort them by
2469 # admin,write,read,none permissions sorted again alphabetically in
2468 # admin,write,read,none permissions sorted again alphabetically in
2470 # each group
2469 # each group
2471 perm_rows = sorted(perm_rows, key=display_sort)
2470 perm_rows = sorted(perm_rows, key=display_sort)
2472
2471
2473 _admin_perm = 'group.admin'
2472 _admin_perm = 'group.admin'
2474 owner_row = []
2473 owner_row = []
2475 if with_owner:
2474 if with_owner:
2476 usr = AttributeDict(self.user.get_dict())
2475 usr = AttributeDict(self.user.get_dict())
2477 usr.owner_row = True
2476 usr.owner_row = True
2478 usr.permission = _admin_perm
2477 usr.permission = _admin_perm
2479 owner_row.append(usr)
2478 owner_row.append(usr)
2480
2479
2481 super_admin_rows = []
2480 super_admin_rows = []
2482 if with_admins:
2481 if with_admins:
2483 for usr in User.get_all_super_admins():
2482 for usr in User.get_all_super_admins():
2484 # if this admin is also owner, don't double the record
2483 # if this admin is also owner, don't double the record
2485 if usr.user_id == owner_row[0].user_id:
2484 if usr.user_id == owner_row[0].user_id:
2486 owner_row[0].admin_row = True
2485 owner_row[0].admin_row = True
2487 else:
2486 else:
2488 usr = AttributeDict(usr.get_dict())
2487 usr = AttributeDict(usr.get_dict())
2489 usr.admin_row = True
2488 usr.admin_row = True
2490 usr.permission = _admin_perm
2489 usr.permission = _admin_perm
2491 super_admin_rows.append(usr)
2490 super_admin_rows.append(usr)
2492
2491
2493 return super_admin_rows + owner_row + perm_rows
2492 return super_admin_rows + owner_row + perm_rows
2494
2493
2495 def permission_user_groups(self):
2494 def permission_user_groups(self):
2496 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2495 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2497 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2496 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2498 joinedload(UserGroupRepoGroupToPerm.users_group),
2497 joinedload(UserGroupRepoGroupToPerm.users_group),
2499 joinedload(UserGroupRepoGroupToPerm.permission),)
2498 joinedload(UserGroupRepoGroupToPerm.permission),)
2500
2499
2501 perm_rows = []
2500 perm_rows = []
2502 for _user_group in q.all():
2501 for _user_group in q.all():
2503 usr = AttributeDict(_user_group.users_group.get_dict())
2502 usr = AttributeDict(_user_group.users_group.get_dict())
2504 usr.permission = _user_group.permission.permission_name
2503 usr.permission = _user_group.permission.permission_name
2505 perm_rows.append(usr)
2504 perm_rows.append(usr)
2506
2505
2507 return perm_rows
2506 return perm_rows
2508
2507
2509 def get_api_data(self):
2508 def get_api_data(self):
2510 """
2509 """
2511 Common function for generating api data
2510 Common function for generating api data
2512
2511
2513 """
2512 """
2514 group = self
2513 group = self
2515 data = {
2514 data = {
2516 'group_id': group.group_id,
2515 'group_id': group.group_id,
2517 'group_name': group.group_name,
2516 'group_name': group.group_name,
2518 'group_description': group.description_safe,
2517 'group_description': group.description_safe,
2519 'parent_group': group.parent_group.group_name if group.parent_group else None,
2518 'parent_group': group.parent_group.group_name if group.parent_group else None,
2520 'repositories': [x.repo_name for x in group.repositories],
2519 'repositories': [x.repo_name for x in group.repositories],
2521 'owner': group.user.username,
2520 'owner': group.user.username,
2522 }
2521 }
2523 return data
2522 return data
2524
2523
2525
2524
2526 class Permission(Base, BaseModel):
2525 class Permission(Base, BaseModel):
2527 __tablename__ = 'permissions'
2526 __tablename__ = 'permissions'
2528 __table_args__ = (
2527 __table_args__ = (
2529 Index('p_perm_name_idx', 'permission_name'),
2528 Index('p_perm_name_idx', 'permission_name'),
2530 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2529 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2531 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2530 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2532 )
2531 )
2533 PERMS = [
2532 PERMS = [
2534 ('hg.admin', _('RhodeCode Super Administrator')),
2533 ('hg.admin', _('RhodeCode Super Administrator')),
2535
2534
2536 ('repository.none', _('Repository no access')),
2535 ('repository.none', _('Repository no access')),
2537 ('repository.read', _('Repository read access')),
2536 ('repository.read', _('Repository read access')),
2538 ('repository.write', _('Repository write access')),
2537 ('repository.write', _('Repository write access')),
2539 ('repository.admin', _('Repository admin access')),
2538 ('repository.admin', _('Repository admin access')),
2540
2539
2541 ('group.none', _('Repository group no access')),
2540 ('group.none', _('Repository group no access')),
2542 ('group.read', _('Repository group read access')),
2541 ('group.read', _('Repository group read access')),
2543 ('group.write', _('Repository group write access')),
2542 ('group.write', _('Repository group write access')),
2544 ('group.admin', _('Repository group admin access')),
2543 ('group.admin', _('Repository group admin access')),
2545
2544
2546 ('usergroup.none', _('User group no access')),
2545 ('usergroup.none', _('User group no access')),
2547 ('usergroup.read', _('User group read access')),
2546 ('usergroup.read', _('User group read access')),
2548 ('usergroup.write', _('User group write access')),
2547 ('usergroup.write', _('User group write access')),
2549 ('usergroup.admin', _('User group admin access')),
2548 ('usergroup.admin', _('User group admin access')),
2550
2549
2551 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2550 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2552 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2551 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2553
2552
2554 ('hg.usergroup.create.false', _('User Group creation disabled')),
2553 ('hg.usergroup.create.false', _('User Group creation disabled')),
2555 ('hg.usergroup.create.true', _('User Group creation enabled')),
2554 ('hg.usergroup.create.true', _('User Group creation enabled')),
2556
2555
2557 ('hg.create.none', _('Repository creation disabled')),
2556 ('hg.create.none', _('Repository creation disabled')),
2558 ('hg.create.repository', _('Repository creation enabled')),
2557 ('hg.create.repository', _('Repository creation enabled')),
2559 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2558 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2560 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2559 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2561
2560
2562 ('hg.fork.none', _('Repository forking disabled')),
2561 ('hg.fork.none', _('Repository forking disabled')),
2563 ('hg.fork.repository', _('Repository forking enabled')),
2562 ('hg.fork.repository', _('Repository forking enabled')),
2564
2563
2565 ('hg.register.none', _('Registration disabled')),
2564 ('hg.register.none', _('Registration disabled')),
2566 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2565 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2567 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2566 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2568
2567
2569 ('hg.password_reset.enabled', _('Password reset enabled')),
2568 ('hg.password_reset.enabled', _('Password reset enabled')),
2570 ('hg.password_reset.hidden', _('Password reset hidden')),
2569 ('hg.password_reset.hidden', _('Password reset hidden')),
2571 ('hg.password_reset.disabled', _('Password reset disabled')),
2570 ('hg.password_reset.disabled', _('Password reset disabled')),
2572
2571
2573 ('hg.extern_activate.manual', _('Manual activation of external account')),
2572 ('hg.extern_activate.manual', _('Manual activation of external account')),
2574 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2573 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2575
2574
2576 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2575 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2577 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2576 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2578 ]
2577 ]
2579
2578
2580 # definition of system default permissions for DEFAULT user
2579 # definition of system default permissions for DEFAULT user
2581 DEFAULT_USER_PERMISSIONS = [
2580 DEFAULT_USER_PERMISSIONS = [
2582 'repository.read',
2581 'repository.read',
2583 'group.read',
2582 'group.read',
2584 'usergroup.read',
2583 'usergroup.read',
2585 'hg.create.repository',
2584 'hg.create.repository',
2586 'hg.repogroup.create.false',
2585 'hg.repogroup.create.false',
2587 'hg.usergroup.create.false',
2586 'hg.usergroup.create.false',
2588 'hg.create.write_on_repogroup.true',
2587 'hg.create.write_on_repogroup.true',
2589 'hg.fork.repository',
2588 'hg.fork.repository',
2590 'hg.register.manual_activate',
2589 'hg.register.manual_activate',
2591 'hg.password_reset.enabled',
2590 'hg.password_reset.enabled',
2592 'hg.extern_activate.auto',
2591 'hg.extern_activate.auto',
2593 'hg.inherit_default_perms.true',
2592 'hg.inherit_default_perms.true',
2594 ]
2593 ]
2595
2594
2596 # defines which permissions are more important higher the more important
2595 # defines which permissions are more important higher the more important
2597 # Weight defines which permissions are more important.
2596 # Weight defines which permissions are more important.
2598 # The higher number the more important.
2597 # The higher number the more important.
2599 PERM_WEIGHTS = {
2598 PERM_WEIGHTS = {
2600 'repository.none': 0,
2599 'repository.none': 0,
2601 'repository.read': 1,
2600 'repository.read': 1,
2602 'repository.write': 3,
2601 'repository.write': 3,
2603 'repository.admin': 4,
2602 'repository.admin': 4,
2604
2603
2605 'group.none': 0,
2604 'group.none': 0,
2606 'group.read': 1,
2605 'group.read': 1,
2607 'group.write': 3,
2606 'group.write': 3,
2608 'group.admin': 4,
2607 'group.admin': 4,
2609
2608
2610 'usergroup.none': 0,
2609 'usergroup.none': 0,
2611 'usergroup.read': 1,
2610 'usergroup.read': 1,
2612 'usergroup.write': 3,
2611 'usergroup.write': 3,
2613 'usergroup.admin': 4,
2612 'usergroup.admin': 4,
2614
2613
2615 'hg.repogroup.create.false': 0,
2614 'hg.repogroup.create.false': 0,
2616 'hg.repogroup.create.true': 1,
2615 'hg.repogroup.create.true': 1,
2617
2616
2618 'hg.usergroup.create.false': 0,
2617 'hg.usergroup.create.false': 0,
2619 'hg.usergroup.create.true': 1,
2618 'hg.usergroup.create.true': 1,
2620
2619
2621 'hg.fork.none': 0,
2620 'hg.fork.none': 0,
2622 'hg.fork.repository': 1,
2621 'hg.fork.repository': 1,
2623 'hg.create.none': 0,
2622 'hg.create.none': 0,
2624 'hg.create.repository': 1
2623 'hg.create.repository': 1
2625 }
2624 }
2626
2625
2627 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2626 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2628 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2627 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2629 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2628 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2630
2629
2631 def __unicode__(self):
2630 def __unicode__(self):
2632 return u"<%s('%s:%s')>" % (
2631 return u"<%s('%s:%s')>" % (
2633 self.__class__.__name__, self.permission_id, self.permission_name
2632 self.__class__.__name__, self.permission_id, self.permission_name
2634 )
2633 )
2635
2634
2636 @classmethod
2635 @classmethod
2637 def get_by_key(cls, key):
2636 def get_by_key(cls, key):
2638 return cls.query().filter(cls.permission_name == key).scalar()
2637 return cls.query().filter(cls.permission_name == key).scalar()
2639
2638
2640 @classmethod
2639 @classmethod
2641 def get_default_repo_perms(cls, user_id, repo_id=None):
2640 def get_default_repo_perms(cls, user_id, repo_id=None):
2642 q = Session().query(UserRepoToPerm, Repository, Permission)\
2641 q = Session().query(UserRepoToPerm, Repository, Permission)\
2643 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2642 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2644 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2643 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2645 .filter(UserRepoToPerm.user_id == user_id)
2644 .filter(UserRepoToPerm.user_id == user_id)
2646 if repo_id:
2645 if repo_id:
2647 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2646 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2648 return q.all()
2647 return q.all()
2649
2648
2650 @classmethod
2649 @classmethod
2651 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2650 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2652 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2651 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2653 .join(
2652 .join(
2654 Permission,
2653 Permission,
2655 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2654 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2656 .join(
2655 .join(
2657 Repository,
2656 Repository,
2658 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2657 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2659 .join(
2658 .join(
2660 UserGroup,
2659 UserGroup,
2661 UserGroupRepoToPerm.users_group_id ==
2660 UserGroupRepoToPerm.users_group_id ==
2662 UserGroup.users_group_id)\
2661 UserGroup.users_group_id)\
2663 .join(
2662 .join(
2664 UserGroupMember,
2663 UserGroupMember,
2665 UserGroupRepoToPerm.users_group_id ==
2664 UserGroupRepoToPerm.users_group_id ==
2666 UserGroupMember.users_group_id)\
2665 UserGroupMember.users_group_id)\
2667 .filter(
2666 .filter(
2668 UserGroupMember.user_id == user_id,
2667 UserGroupMember.user_id == user_id,
2669 UserGroup.users_group_active == true())
2668 UserGroup.users_group_active == true())
2670 if repo_id:
2669 if repo_id:
2671 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2670 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2672 return q.all()
2671 return q.all()
2673
2672
2674 @classmethod
2673 @classmethod
2675 def get_default_group_perms(cls, user_id, repo_group_id=None):
2674 def get_default_group_perms(cls, user_id, repo_group_id=None):
2676 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2675 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2677 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2676 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2678 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2677 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2679 .filter(UserRepoGroupToPerm.user_id == user_id)
2678 .filter(UserRepoGroupToPerm.user_id == user_id)
2680 if repo_group_id:
2679 if repo_group_id:
2681 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2680 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2682 return q.all()
2681 return q.all()
2683
2682
2684 @classmethod
2683 @classmethod
2685 def get_default_group_perms_from_user_group(
2684 def get_default_group_perms_from_user_group(
2686 cls, user_id, repo_group_id=None):
2685 cls, user_id, repo_group_id=None):
2687 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2686 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2688 .join(
2687 .join(
2689 Permission,
2688 Permission,
2690 UserGroupRepoGroupToPerm.permission_id ==
2689 UserGroupRepoGroupToPerm.permission_id ==
2691 Permission.permission_id)\
2690 Permission.permission_id)\
2692 .join(
2691 .join(
2693 RepoGroup,
2692 RepoGroup,
2694 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2693 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2695 .join(
2694 .join(
2696 UserGroup,
2695 UserGroup,
2697 UserGroupRepoGroupToPerm.users_group_id ==
2696 UserGroupRepoGroupToPerm.users_group_id ==
2698 UserGroup.users_group_id)\
2697 UserGroup.users_group_id)\
2699 .join(
2698 .join(
2700 UserGroupMember,
2699 UserGroupMember,
2701 UserGroupRepoGroupToPerm.users_group_id ==
2700 UserGroupRepoGroupToPerm.users_group_id ==
2702 UserGroupMember.users_group_id)\
2701 UserGroupMember.users_group_id)\
2703 .filter(
2702 .filter(
2704 UserGroupMember.user_id == user_id,
2703 UserGroupMember.user_id == user_id,
2705 UserGroup.users_group_active == true())
2704 UserGroup.users_group_active == true())
2706 if repo_group_id:
2705 if repo_group_id:
2707 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2706 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2708 return q.all()
2707 return q.all()
2709
2708
2710 @classmethod
2709 @classmethod
2711 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2710 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2712 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2711 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2713 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2712 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2714 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2713 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2715 .filter(UserUserGroupToPerm.user_id == user_id)
2714 .filter(UserUserGroupToPerm.user_id == user_id)
2716 if user_group_id:
2715 if user_group_id:
2717 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2716 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2718 return q.all()
2717 return q.all()
2719
2718
2720 @classmethod
2719 @classmethod
2721 def get_default_user_group_perms_from_user_group(
2720 def get_default_user_group_perms_from_user_group(
2722 cls, user_id, user_group_id=None):
2721 cls, user_id, user_group_id=None):
2723 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2722 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2724 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2723 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2725 .join(
2724 .join(
2726 Permission,
2725 Permission,
2727 UserGroupUserGroupToPerm.permission_id ==
2726 UserGroupUserGroupToPerm.permission_id ==
2728 Permission.permission_id)\
2727 Permission.permission_id)\
2729 .join(
2728 .join(
2730 TargetUserGroup,
2729 TargetUserGroup,
2731 UserGroupUserGroupToPerm.target_user_group_id ==
2730 UserGroupUserGroupToPerm.target_user_group_id ==
2732 TargetUserGroup.users_group_id)\
2731 TargetUserGroup.users_group_id)\
2733 .join(
2732 .join(
2734 UserGroup,
2733 UserGroup,
2735 UserGroupUserGroupToPerm.user_group_id ==
2734 UserGroupUserGroupToPerm.user_group_id ==
2736 UserGroup.users_group_id)\
2735 UserGroup.users_group_id)\
2737 .join(
2736 .join(
2738 UserGroupMember,
2737 UserGroupMember,
2739 UserGroupUserGroupToPerm.user_group_id ==
2738 UserGroupUserGroupToPerm.user_group_id ==
2740 UserGroupMember.users_group_id)\
2739 UserGroupMember.users_group_id)\
2741 .filter(
2740 .filter(
2742 UserGroupMember.user_id == user_id,
2741 UserGroupMember.user_id == user_id,
2743 UserGroup.users_group_active == true())
2742 UserGroup.users_group_active == true())
2744 if user_group_id:
2743 if user_group_id:
2745 q = q.filter(
2744 q = q.filter(
2746 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2745 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2747
2746
2748 return q.all()
2747 return q.all()
2749
2748
2750
2749
2751 class UserRepoToPerm(Base, BaseModel):
2750 class UserRepoToPerm(Base, BaseModel):
2752 __tablename__ = 'repo_to_perm'
2751 __tablename__ = 'repo_to_perm'
2753 __table_args__ = (
2752 __table_args__ = (
2754 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2753 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2755 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2754 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2756 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2755 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2757 )
2756 )
2758 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2757 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2759 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2758 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2760 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2759 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2761 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2760 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2762
2761
2763 user = relationship('User')
2762 user = relationship('User')
2764 repository = relationship('Repository')
2763 repository = relationship('Repository')
2765 permission = relationship('Permission')
2764 permission = relationship('Permission')
2766
2765
2767 @classmethod
2766 @classmethod
2768 def create(cls, user, repository, permission):
2767 def create(cls, user, repository, permission):
2769 n = cls()
2768 n = cls()
2770 n.user = user
2769 n.user = user
2771 n.repository = repository
2770 n.repository = repository
2772 n.permission = permission
2771 n.permission = permission
2773 Session().add(n)
2772 Session().add(n)
2774 return n
2773 return n
2775
2774
2776 def __unicode__(self):
2775 def __unicode__(self):
2777 return u'<%s => %s >' % (self.user, self.repository)
2776 return u'<%s => %s >' % (self.user, self.repository)
2778
2777
2779
2778
2780 class UserUserGroupToPerm(Base, BaseModel):
2779 class UserUserGroupToPerm(Base, BaseModel):
2781 __tablename__ = 'user_user_group_to_perm'
2780 __tablename__ = 'user_user_group_to_perm'
2782 __table_args__ = (
2781 __table_args__ = (
2783 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2782 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2784 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2783 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2785 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2784 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2786 )
2785 )
2787 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2786 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2788 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2787 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2789 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2788 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2790 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2789 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2791
2790
2792 user = relationship('User')
2791 user = relationship('User')
2793 user_group = relationship('UserGroup')
2792 user_group = relationship('UserGroup')
2794 permission = relationship('Permission')
2793 permission = relationship('Permission')
2795
2794
2796 @classmethod
2795 @classmethod
2797 def create(cls, user, user_group, permission):
2796 def create(cls, user, user_group, permission):
2798 n = cls()
2797 n = cls()
2799 n.user = user
2798 n.user = user
2800 n.user_group = user_group
2799 n.user_group = user_group
2801 n.permission = permission
2800 n.permission = permission
2802 Session().add(n)
2801 Session().add(n)
2803 return n
2802 return n
2804
2803
2805 def __unicode__(self):
2804 def __unicode__(self):
2806 return u'<%s => %s >' % (self.user, self.user_group)
2805 return u'<%s => %s >' % (self.user, self.user_group)
2807
2806
2808
2807
2809 class UserToPerm(Base, BaseModel):
2808 class UserToPerm(Base, BaseModel):
2810 __tablename__ = 'user_to_perm'
2809 __tablename__ = 'user_to_perm'
2811 __table_args__ = (
2810 __table_args__ = (
2812 UniqueConstraint('user_id', 'permission_id'),
2811 UniqueConstraint('user_id', 'permission_id'),
2813 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2812 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2814 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2813 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2815 )
2814 )
2816 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2815 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2817 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2816 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2818 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2817 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2819
2818
2820 user = relationship('User')
2819 user = relationship('User')
2821 permission = relationship('Permission', lazy='joined')
2820 permission = relationship('Permission', lazy='joined')
2822
2821
2823 def __unicode__(self):
2822 def __unicode__(self):
2824 return u'<%s => %s >' % (self.user, self.permission)
2823 return u'<%s => %s >' % (self.user, self.permission)
2825
2824
2826
2825
2827 class UserGroupRepoToPerm(Base, BaseModel):
2826 class UserGroupRepoToPerm(Base, BaseModel):
2828 __tablename__ = 'users_group_repo_to_perm'
2827 __tablename__ = 'users_group_repo_to_perm'
2829 __table_args__ = (
2828 __table_args__ = (
2830 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2829 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2831 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2830 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2832 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2831 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2833 )
2832 )
2834 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2833 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2835 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2834 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2836 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2835 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2837 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2836 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2838
2837
2839 users_group = relationship('UserGroup')
2838 users_group = relationship('UserGroup')
2840 permission = relationship('Permission')
2839 permission = relationship('Permission')
2841 repository = relationship('Repository')
2840 repository = relationship('Repository')
2842
2841
2843 @classmethod
2842 @classmethod
2844 def create(cls, users_group, repository, permission):
2843 def create(cls, users_group, repository, permission):
2845 n = cls()
2844 n = cls()
2846 n.users_group = users_group
2845 n.users_group = users_group
2847 n.repository = repository
2846 n.repository = repository
2848 n.permission = permission
2847 n.permission = permission
2849 Session().add(n)
2848 Session().add(n)
2850 return n
2849 return n
2851
2850
2852 def __unicode__(self):
2851 def __unicode__(self):
2853 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2852 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2854
2853
2855
2854
2856 class UserGroupUserGroupToPerm(Base, BaseModel):
2855 class UserGroupUserGroupToPerm(Base, BaseModel):
2857 __tablename__ = 'user_group_user_group_to_perm'
2856 __tablename__ = 'user_group_user_group_to_perm'
2858 __table_args__ = (
2857 __table_args__ = (
2859 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2858 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2860 CheckConstraint('target_user_group_id != user_group_id'),
2859 CheckConstraint('target_user_group_id != user_group_id'),
2861 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2860 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2862 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2861 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2863 )
2862 )
2864 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2863 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2865 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2864 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2866 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2865 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2867 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2866 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2868
2867
2869 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2868 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2870 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2869 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2871 permission = relationship('Permission')
2870 permission = relationship('Permission')
2872
2871
2873 @classmethod
2872 @classmethod
2874 def create(cls, target_user_group, user_group, permission):
2873 def create(cls, target_user_group, user_group, permission):
2875 n = cls()
2874 n = cls()
2876 n.target_user_group = target_user_group
2875 n.target_user_group = target_user_group
2877 n.user_group = user_group
2876 n.user_group = user_group
2878 n.permission = permission
2877 n.permission = permission
2879 Session().add(n)
2878 Session().add(n)
2880 return n
2879 return n
2881
2880
2882 def __unicode__(self):
2881 def __unicode__(self):
2883 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2882 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2884
2883
2885
2884
2886 class UserGroupToPerm(Base, BaseModel):
2885 class UserGroupToPerm(Base, BaseModel):
2887 __tablename__ = 'users_group_to_perm'
2886 __tablename__ = 'users_group_to_perm'
2888 __table_args__ = (
2887 __table_args__ = (
2889 UniqueConstraint('users_group_id', 'permission_id',),
2888 UniqueConstraint('users_group_id', 'permission_id',),
2890 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2889 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2891 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2890 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2892 )
2891 )
2893 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2892 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2894 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2893 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2895 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2894 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2896
2895
2897 users_group = relationship('UserGroup')
2896 users_group = relationship('UserGroup')
2898 permission = relationship('Permission')
2897 permission = relationship('Permission')
2899
2898
2900
2899
2901 class UserRepoGroupToPerm(Base, BaseModel):
2900 class UserRepoGroupToPerm(Base, BaseModel):
2902 __tablename__ = 'user_repo_group_to_perm'
2901 __tablename__ = 'user_repo_group_to_perm'
2903 __table_args__ = (
2902 __table_args__ = (
2904 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2903 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2905 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2904 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2906 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2905 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2907 )
2906 )
2908
2907
2909 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2908 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2910 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2909 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2911 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2910 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2912 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2911 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2913
2912
2914 user = relationship('User')
2913 user = relationship('User')
2915 group = relationship('RepoGroup')
2914 group = relationship('RepoGroup')
2916 permission = relationship('Permission')
2915 permission = relationship('Permission')
2917
2916
2918 @classmethod
2917 @classmethod
2919 def create(cls, user, repository_group, permission):
2918 def create(cls, user, repository_group, permission):
2920 n = cls()
2919 n = cls()
2921 n.user = user
2920 n.user = user
2922 n.group = repository_group
2921 n.group = repository_group
2923 n.permission = permission
2922 n.permission = permission
2924 Session().add(n)
2923 Session().add(n)
2925 return n
2924 return n
2926
2925
2927
2926
2928 class UserGroupRepoGroupToPerm(Base, BaseModel):
2927 class UserGroupRepoGroupToPerm(Base, BaseModel):
2929 __tablename__ = 'users_group_repo_group_to_perm'
2928 __tablename__ = 'users_group_repo_group_to_perm'
2930 __table_args__ = (
2929 __table_args__ = (
2931 UniqueConstraint('users_group_id', 'group_id'),
2930 UniqueConstraint('users_group_id', 'group_id'),
2932 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2931 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2933 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2932 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2934 )
2933 )
2935
2934
2936 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2935 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2937 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2936 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2938 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2937 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2939 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2938 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2940
2939
2941 users_group = relationship('UserGroup')
2940 users_group = relationship('UserGroup')
2942 permission = relationship('Permission')
2941 permission = relationship('Permission')
2943 group = relationship('RepoGroup')
2942 group = relationship('RepoGroup')
2944
2943
2945 @classmethod
2944 @classmethod
2946 def create(cls, user_group, repository_group, permission):
2945 def create(cls, user_group, repository_group, permission):
2947 n = cls()
2946 n = cls()
2948 n.users_group = user_group
2947 n.users_group = user_group
2949 n.group = repository_group
2948 n.group = repository_group
2950 n.permission = permission
2949 n.permission = permission
2951 Session().add(n)
2950 Session().add(n)
2952 return n
2951 return n
2953
2952
2954 def __unicode__(self):
2953 def __unicode__(self):
2955 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2954 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2956
2955
2957
2956
2958 class Statistics(Base, BaseModel):
2957 class Statistics(Base, BaseModel):
2959 __tablename__ = 'statistics'
2958 __tablename__ = 'statistics'
2960 __table_args__ = (
2959 __table_args__ = (
2961 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2960 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2962 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2961 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2963 )
2962 )
2964 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2963 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2965 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2964 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2966 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2965 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2967 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2966 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2968 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2967 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2969 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2968 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2970
2969
2971 repository = relationship('Repository', single_parent=True)
2970 repository = relationship('Repository', single_parent=True)
2972
2971
2973
2972
2974 class UserFollowing(Base, BaseModel):
2973 class UserFollowing(Base, BaseModel):
2975 __tablename__ = 'user_followings'
2974 __tablename__ = 'user_followings'
2976 __table_args__ = (
2975 __table_args__ = (
2977 UniqueConstraint('user_id', 'follows_repository_id'),
2976 UniqueConstraint('user_id', 'follows_repository_id'),
2978 UniqueConstraint('user_id', 'follows_user_id'),
2977 UniqueConstraint('user_id', 'follows_user_id'),
2979 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2978 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2980 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2979 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2981 )
2980 )
2982
2981
2983 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2982 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2984 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2983 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2985 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2984 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2986 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2985 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2987 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2986 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2988
2987
2989 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2988 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2990
2989
2991 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2990 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2992 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2991 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2993
2992
2994 @classmethod
2993 @classmethod
2995 def get_repo_followers(cls, repo_id):
2994 def get_repo_followers(cls, repo_id):
2996 return cls.query().filter(cls.follows_repo_id == repo_id)
2995 return cls.query().filter(cls.follows_repo_id == repo_id)
2997
2996
2998
2997
2999 class CacheKey(Base, BaseModel):
2998 class CacheKey(Base, BaseModel):
3000 __tablename__ = 'cache_invalidation'
2999 __tablename__ = 'cache_invalidation'
3001 __table_args__ = (
3000 __table_args__ = (
3002 UniqueConstraint('cache_key'),
3001 UniqueConstraint('cache_key'),
3003 Index('key_idx', 'cache_key'),
3002 Index('key_idx', 'cache_key'),
3004 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3003 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3005 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3004 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3006 )
3005 )
3007 CACHE_TYPE_ATOM = 'ATOM'
3006 CACHE_TYPE_ATOM = 'ATOM'
3008 CACHE_TYPE_RSS = 'RSS'
3007 CACHE_TYPE_RSS = 'RSS'
3009 CACHE_TYPE_README = 'README'
3008 CACHE_TYPE_README = 'README'
3010
3009
3011 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3010 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3012 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3011 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3013 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3012 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3014 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3013 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3015
3014
3016 def __init__(self, cache_key, cache_args=''):
3015 def __init__(self, cache_key, cache_args=''):
3017 self.cache_key = cache_key
3016 self.cache_key = cache_key
3018 self.cache_args = cache_args
3017 self.cache_args = cache_args
3019 self.cache_active = False
3018 self.cache_active = False
3020
3019
3021 def __unicode__(self):
3020 def __unicode__(self):
3022 return u"<%s('%s:%s[%s]')>" % (
3021 return u"<%s('%s:%s[%s]')>" % (
3023 self.__class__.__name__,
3022 self.__class__.__name__,
3024 self.cache_id, self.cache_key, self.cache_active)
3023 self.cache_id, self.cache_key, self.cache_active)
3025
3024
3026 def _cache_key_partition(self):
3025 def _cache_key_partition(self):
3027 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3026 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3028 return prefix, repo_name, suffix
3027 return prefix, repo_name, suffix
3029
3028
3030 def get_prefix(self):
3029 def get_prefix(self):
3031 """
3030 """
3032 Try to extract prefix from existing cache key. The key could consist
3031 Try to extract prefix from existing cache key. The key could consist
3033 of prefix, repo_name, suffix
3032 of prefix, repo_name, suffix
3034 """
3033 """
3035 # this returns prefix, repo_name, suffix
3034 # this returns prefix, repo_name, suffix
3036 return self._cache_key_partition()[0]
3035 return self._cache_key_partition()[0]
3037
3036
3038 def get_suffix(self):
3037 def get_suffix(self):
3039 """
3038 """
3040 get suffix that might have been used in _get_cache_key to
3039 get suffix that might have been used in _get_cache_key to
3041 generate self.cache_key. Only used for informational purposes
3040 generate self.cache_key. Only used for informational purposes
3042 in repo_edit.mako.
3041 in repo_edit.mako.
3043 """
3042 """
3044 # prefix, repo_name, suffix
3043 # prefix, repo_name, suffix
3045 return self._cache_key_partition()[2]
3044 return self._cache_key_partition()[2]
3046
3045
3047 @classmethod
3046 @classmethod
3048 def delete_all_cache(cls):
3047 def delete_all_cache(cls):
3049 """
3048 """
3050 Delete all cache keys from database.
3049 Delete all cache keys from database.
3051 Should only be run when all instances are down and all entries
3050 Should only be run when all instances are down and all entries
3052 thus stale.
3051 thus stale.
3053 """
3052 """
3054 cls.query().delete()
3053 cls.query().delete()
3055 Session().commit()
3054 Session().commit()
3056
3055
3057 @classmethod
3056 @classmethod
3058 def get_cache_key(cls, repo_name, cache_type):
3057 def get_cache_key(cls, repo_name, cache_type):
3059 """
3058 """
3060
3059
3061 Generate a cache key for this process of RhodeCode instance.
3060 Generate a cache key for this process of RhodeCode instance.
3062 Prefix most likely will be process id or maybe explicitly set
3061 Prefix most likely will be process id or maybe explicitly set
3063 instance_id from .ini file.
3062 instance_id from .ini file.
3064 """
3063 """
3065 import rhodecode
3064 import rhodecode
3066 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
3065 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
3067
3066
3068 repo_as_unicode = safe_unicode(repo_name)
3067 repo_as_unicode = safe_unicode(repo_name)
3069 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
3068 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
3070 if cache_type else repo_as_unicode
3069 if cache_type else repo_as_unicode
3071
3070
3072 return u'{}{}'.format(prefix, key)
3071 return u'{}{}'.format(prefix, key)
3073
3072
3074 @classmethod
3073 @classmethod
3075 def set_invalidate(cls, repo_name, delete=False):
3074 def set_invalidate(cls, repo_name, delete=False):
3076 """
3075 """
3077 Mark all caches of a repo as invalid in the database.
3076 Mark all caches of a repo as invalid in the database.
3078 """
3077 """
3079
3078
3080 try:
3079 try:
3081 qry = Session().query(cls).filter(cls.cache_args == repo_name)
3080 qry = Session().query(cls).filter(cls.cache_args == repo_name)
3082 if delete:
3081 if delete:
3083 log.debug('cache objects deleted for repo %s',
3082 log.debug('cache objects deleted for repo %s',
3084 safe_str(repo_name))
3083 safe_str(repo_name))
3085 qry.delete()
3084 qry.delete()
3086 else:
3085 else:
3087 log.debug('cache objects marked as invalid for repo %s',
3086 log.debug('cache objects marked as invalid for repo %s',
3088 safe_str(repo_name))
3087 safe_str(repo_name))
3089 qry.update({"cache_active": False})
3088 qry.update({"cache_active": False})
3090
3089
3091 Session().commit()
3090 Session().commit()
3092 except Exception:
3091 except Exception:
3093 log.exception(
3092 log.exception(
3094 'Cache key invalidation failed for repository %s',
3093 'Cache key invalidation failed for repository %s',
3095 safe_str(repo_name))
3094 safe_str(repo_name))
3096 Session().rollback()
3095 Session().rollback()
3097
3096
3098 @classmethod
3097 @classmethod
3099 def get_active_cache(cls, cache_key):
3098 def get_active_cache(cls, cache_key):
3100 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3099 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3101 if inv_obj:
3100 if inv_obj:
3102 return inv_obj
3101 return inv_obj
3103 return None
3102 return None
3104
3103
3105 @classmethod
3104 @classmethod
3106 def repo_context_cache(cls, compute_func, repo_name, cache_type,
3105 def repo_context_cache(cls, compute_func, repo_name, cache_type,
3107 thread_scoped=False):
3106 thread_scoped=False):
3108 """
3107 """
3109 @cache_region('long_term')
3108 @cache_region('long_term')
3110 def _heavy_calculation(cache_key):
3109 def _heavy_calculation(cache_key):
3111 return 'result'
3110 return 'result'
3112
3111
3113 cache_context = CacheKey.repo_context_cache(
3112 cache_context = CacheKey.repo_context_cache(
3114 _heavy_calculation, repo_name, cache_type)
3113 _heavy_calculation, repo_name, cache_type)
3115
3114
3116 with cache_context as context:
3115 with cache_context as context:
3117 context.invalidate()
3116 context.invalidate()
3118 computed = context.compute()
3117 computed = context.compute()
3119
3118
3120 assert computed == 'result'
3119 assert computed == 'result'
3121 """
3120 """
3122 from rhodecode.lib import caches
3121 from rhodecode.lib import caches
3123 return caches.InvalidationContext(
3122 return caches.InvalidationContext(
3124 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
3123 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
3125
3124
3126
3125
3127 class ChangesetComment(Base, BaseModel):
3126 class ChangesetComment(Base, BaseModel):
3128 __tablename__ = 'changeset_comments'
3127 __tablename__ = 'changeset_comments'
3129 __table_args__ = (
3128 __table_args__ = (
3130 Index('cc_revision_idx', 'revision'),
3129 Index('cc_revision_idx', 'revision'),
3131 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3130 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3132 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3131 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3133 )
3132 )
3134
3133
3135 COMMENT_OUTDATED = u'comment_outdated'
3134 COMMENT_OUTDATED = u'comment_outdated'
3136 COMMENT_TYPE_NOTE = u'note'
3135 COMMENT_TYPE_NOTE = u'note'
3137 COMMENT_TYPE_TODO = u'todo'
3136 COMMENT_TYPE_TODO = u'todo'
3138 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3137 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3139
3138
3140 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3139 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3141 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3140 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3142 revision = Column('revision', String(40), nullable=True)
3141 revision = Column('revision', String(40), nullable=True)
3143 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3142 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3144 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3143 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3145 line_no = Column('line_no', Unicode(10), nullable=True)
3144 line_no = Column('line_no', Unicode(10), nullable=True)
3146 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3145 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3147 f_path = Column('f_path', Unicode(1000), nullable=True)
3146 f_path = Column('f_path', Unicode(1000), nullable=True)
3148 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3147 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3149 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3148 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3150 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3149 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3151 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3150 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3152 renderer = Column('renderer', Unicode(64), nullable=True)
3151 renderer = Column('renderer', Unicode(64), nullable=True)
3153 display_state = Column('display_state', Unicode(128), nullable=True)
3152 display_state = Column('display_state', Unicode(128), nullable=True)
3154
3153
3155 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3154 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3156 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3155 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3157 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3156 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3158 author = relationship('User', lazy='joined')
3157 author = relationship('User', lazy='joined')
3159 repo = relationship('Repository')
3158 repo = relationship('Repository')
3160 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3159 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3161 pull_request = relationship('PullRequest', lazy='joined')
3160 pull_request = relationship('PullRequest', lazy='joined')
3162 pull_request_version = relationship('PullRequestVersion')
3161 pull_request_version = relationship('PullRequestVersion')
3163
3162
3164 @classmethod
3163 @classmethod
3165 def get_users(cls, revision=None, pull_request_id=None):
3164 def get_users(cls, revision=None, pull_request_id=None):
3166 """
3165 """
3167 Returns user associated with this ChangesetComment. ie those
3166 Returns user associated with this ChangesetComment. ie those
3168 who actually commented
3167 who actually commented
3169
3168
3170 :param cls:
3169 :param cls:
3171 :param revision:
3170 :param revision:
3172 """
3171 """
3173 q = Session().query(User)\
3172 q = Session().query(User)\
3174 .join(ChangesetComment.author)
3173 .join(ChangesetComment.author)
3175 if revision:
3174 if revision:
3176 q = q.filter(cls.revision == revision)
3175 q = q.filter(cls.revision == revision)
3177 elif pull_request_id:
3176 elif pull_request_id:
3178 q = q.filter(cls.pull_request_id == pull_request_id)
3177 q = q.filter(cls.pull_request_id == pull_request_id)
3179 return q.all()
3178 return q.all()
3180
3179
3181 @classmethod
3180 @classmethod
3182 def get_index_from_version(cls, pr_version, versions):
3181 def get_index_from_version(cls, pr_version, versions):
3183 num_versions = [x.pull_request_version_id for x in versions]
3182 num_versions = [x.pull_request_version_id for x in versions]
3184 try:
3183 try:
3185 return num_versions.index(pr_version) +1
3184 return num_versions.index(pr_version) +1
3186 except (IndexError, ValueError):
3185 except (IndexError, ValueError):
3187 return
3186 return
3188
3187
3189 @property
3188 @property
3190 def outdated(self):
3189 def outdated(self):
3191 return self.display_state == self.COMMENT_OUTDATED
3190 return self.display_state == self.COMMENT_OUTDATED
3192
3191
3193 def outdated_at_version(self, version):
3192 def outdated_at_version(self, version):
3194 """
3193 """
3195 Checks if comment is outdated for given pull request version
3194 Checks if comment is outdated for given pull request version
3196 """
3195 """
3197 return self.outdated and self.pull_request_version_id != version
3196 return self.outdated and self.pull_request_version_id != version
3198
3197
3199 def older_than_version(self, version):
3198 def older_than_version(self, version):
3200 """
3199 """
3201 Checks if comment is made from previous version than given
3200 Checks if comment is made from previous version than given
3202 """
3201 """
3203 if version is None:
3202 if version is None:
3204 return self.pull_request_version_id is not None
3203 return self.pull_request_version_id is not None
3205
3204
3206 return self.pull_request_version_id < version
3205 return self.pull_request_version_id < version
3207
3206
3208 @property
3207 @property
3209 def resolved(self):
3208 def resolved(self):
3210 return self.resolved_by[0] if self.resolved_by else None
3209 return self.resolved_by[0] if self.resolved_by else None
3211
3210
3212 @property
3211 @property
3213 def is_todo(self):
3212 def is_todo(self):
3214 return self.comment_type == self.COMMENT_TYPE_TODO
3213 return self.comment_type == self.COMMENT_TYPE_TODO
3215
3214
3216 @property
3215 @property
3217 def is_inline(self):
3216 def is_inline(self):
3218 return self.line_no and self.f_path
3217 return self.line_no and self.f_path
3219
3218
3220 def get_index_version(self, versions):
3219 def get_index_version(self, versions):
3221 return self.get_index_from_version(
3220 return self.get_index_from_version(
3222 self.pull_request_version_id, versions)
3221 self.pull_request_version_id, versions)
3223
3222
3224 def __repr__(self):
3223 def __repr__(self):
3225 if self.comment_id:
3224 if self.comment_id:
3226 return '<DB:Comment #%s>' % self.comment_id
3225 return '<DB:Comment #%s>' % self.comment_id
3227 else:
3226 else:
3228 return '<DB:Comment at %#x>' % id(self)
3227 return '<DB:Comment at %#x>' % id(self)
3229
3228
3230 def get_api_data(self):
3229 def get_api_data(self):
3231 comment = self
3230 comment = self
3232 data = {
3231 data = {
3233 'comment_id': comment.comment_id,
3232 'comment_id': comment.comment_id,
3234 'comment_type': comment.comment_type,
3233 'comment_type': comment.comment_type,
3235 'comment_text': comment.text,
3234 'comment_text': comment.text,
3236 'comment_status': comment.status_change,
3235 'comment_status': comment.status_change,
3237 'comment_f_path': comment.f_path,
3236 'comment_f_path': comment.f_path,
3238 'comment_lineno': comment.line_no,
3237 'comment_lineno': comment.line_no,
3239 'comment_author': comment.author,
3238 'comment_author': comment.author,
3240 'comment_created_on': comment.created_on
3239 'comment_created_on': comment.created_on
3241 }
3240 }
3242 return data
3241 return data
3243
3242
3244 def __json__(self):
3243 def __json__(self):
3245 data = dict()
3244 data = dict()
3246 data.update(self.get_api_data())
3245 data.update(self.get_api_data())
3247 return data
3246 return data
3248
3247
3249
3248
3250 class ChangesetStatus(Base, BaseModel):
3249 class ChangesetStatus(Base, BaseModel):
3251 __tablename__ = 'changeset_statuses'
3250 __tablename__ = 'changeset_statuses'
3252 __table_args__ = (
3251 __table_args__ = (
3253 Index('cs_revision_idx', 'revision'),
3252 Index('cs_revision_idx', 'revision'),
3254 Index('cs_version_idx', 'version'),
3253 Index('cs_version_idx', 'version'),
3255 UniqueConstraint('repo_id', 'revision', 'version'),
3254 UniqueConstraint('repo_id', 'revision', 'version'),
3256 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3255 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3257 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3256 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3258 )
3257 )
3259 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3258 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3260 STATUS_APPROVED = 'approved'
3259 STATUS_APPROVED = 'approved'
3261 STATUS_REJECTED = 'rejected'
3260 STATUS_REJECTED = 'rejected'
3262 STATUS_UNDER_REVIEW = 'under_review'
3261 STATUS_UNDER_REVIEW = 'under_review'
3263
3262
3264 STATUSES = [
3263 STATUSES = [
3265 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3264 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3266 (STATUS_APPROVED, _("Approved")),
3265 (STATUS_APPROVED, _("Approved")),
3267 (STATUS_REJECTED, _("Rejected")),
3266 (STATUS_REJECTED, _("Rejected")),
3268 (STATUS_UNDER_REVIEW, _("Under Review")),
3267 (STATUS_UNDER_REVIEW, _("Under Review")),
3269 ]
3268 ]
3270
3269
3271 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3270 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3272 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3271 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3273 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3272 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3274 revision = Column('revision', String(40), nullable=False)
3273 revision = Column('revision', String(40), nullable=False)
3275 status = Column('status', String(128), nullable=False, default=DEFAULT)
3274 status = Column('status', String(128), nullable=False, default=DEFAULT)
3276 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3275 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3277 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3276 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3278 version = Column('version', Integer(), nullable=False, default=0)
3277 version = Column('version', Integer(), nullable=False, default=0)
3279 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3278 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3280
3279
3281 author = relationship('User', lazy='joined')
3280 author = relationship('User', lazy='joined')
3282 repo = relationship('Repository')
3281 repo = relationship('Repository')
3283 comment = relationship('ChangesetComment', lazy='joined')
3282 comment = relationship('ChangesetComment', lazy='joined')
3284 pull_request = relationship('PullRequest', lazy='joined')
3283 pull_request = relationship('PullRequest', lazy='joined')
3285
3284
3286 def __unicode__(self):
3285 def __unicode__(self):
3287 return u"<%s('%s[v%s]:%s')>" % (
3286 return u"<%s('%s[v%s]:%s')>" % (
3288 self.__class__.__name__,
3287 self.__class__.__name__,
3289 self.status, self.version, self.author
3288 self.status, self.version, self.author
3290 )
3289 )
3291
3290
3292 @classmethod
3291 @classmethod
3293 def get_status_lbl(cls, value):
3292 def get_status_lbl(cls, value):
3294 return dict(cls.STATUSES).get(value)
3293 return dict(cls.STATUSES).get(value)
3295
3294
3296 @property
3295 @property
3297 def status_lbl(self):
3296 def status_lbl(self):
3298 return ChangesetStatus.get_status_lbl(self.status)
3297 return ChangesetStatus.get_status_lbl(self.status)
3299
3298
3300 def get_api_data(self):
3299 def get_api_data(self):
3301 status = self
3300 status = self
3302 data = {
3301 data = {
3303 'status_id': status.changeset_status_id,
3302 'status_id': status.changeset_status_id,
3304 'status': status.status,
3303 'status': status.status,
3305 }
3304 }
3306 return data
3305 return data
3307
3306
3308 def __json__(self):
3307 def __json__(self):
3309 data = dict()
3308 data = dict()
3310 data.update(self.get_api_data())
3309 data.update(self.get_api_data())
3311 return data
3310 return data
3312
3311
3313
3312
3314 class _PullRequestBase(BaseModel):
3313 class _PullRequestBase(BaseModel):
3315 """
3314 """
3316 Common attributes of pull request and version entries.
3315 Common attributes of pull request and version entries.
3317 """
3316 """
3318
3317
3319 # .status values
3318 # .status values
3320 STATUS_NEW = u'new'
3319 STATUS_NEW = u'new'
3321 STATUS_OPEN = u'open'
3320 STATUS_OPEN = u'open'
3322 STATUS_CLOSED = u'closed'
3321 STATUS_CLOSED = u'closed'
3323
3322
3324 title = Column('title', Unicode(255), nullable=True)
3323 title = Column('title', Unicode(255), nullable=True)
3325 description = Column(
3324 description = Column(
3326 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3325 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3327 nullable=True)
3326 nullable=True)
3328 # new/open/closed status of pull request (not approve/reject/etc)
3327 # new/open/closed status of pull request (not approve/reject/etc)
3329 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3328 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3330 created_on = Column(
3329 created_on = Column(
3331 'created_on', DateTime(timezone=False), nullable=False,
3330 'created_on', DateTime(timezone=False), nullable=False,
3332 default=datetime.datetime.now)
3331 default=datetime.datetime.now)
3333 updated_on = Column(
3332 updated_on = Column(
3334 'updated_on', DateTime(timezone=False), nullable=False,
3333 'updated_on', DateTime(timezone=False), nullable=False,
3335 default=datetime.datetime.now)
3334 default=datetime.datetime.now)
3336
3335
3337 @declared_attr
3336 @declared_attr
3338 def user_id(cls):
3337 def user_id(cls):
3339 return Column(
3338 return Column(
3340 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3339 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3341 unique=None)
3340 unique=None)
3342
3341
3343 # 500 revisions max
3342 # 500 revisions max
3344 _revisions = Column(
3343 _revisions = Column(
3345 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3344 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3346
3345
3347 @declared_attr
3346 @declared_attr
3348 def source_repo_id(cls):
3347 def source_repo_id(cls):
3349 # TODO: dan: rename column to source_repo_id
3348 # TODO: dan: rename column to source_repo_id
3350 return Column(
3349 return Column(
3351 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3350 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3352 nullable=False)
3351 nullable=False)
3353
3352
3354 source_ref = Column('org_ref', Unicode(255), nullable=False)
3353 source_ref = Column('org_ref', Unicode(255), nullable=False)
3355
3354
3356 @declared_attr
3355 @declared_attr
3357 def target_repo_id(cls):
3356 def target_repo_id(cls):
3358 # TODO: dan: rename column to target_repo_id
3357 # TODO: dan: rename column to target_repo_id
3359 return Column(
3358 return Column(
3360 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3359 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3361 nullable=False)
3360 nullable=False)
3362
3361
3363 target_ref = Column('other_ref', Unicode(255), nullable=False)
3362 target_ref = Column('other_ref', Unicode(255), nullable=False)
3364 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3363 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3365
3364
3366 # TODO: dan: rename column to last_merge_source_rev
3365 # TODO: dan: rename column to last_merge_source_rev
3367 _last_merge_source_rev = Column(
3366 _last_merge_source_rev = Column(
3368 'last_merge_org_rev', String(40), nullable=True)
3367 'last_merge_org_rev', String(40), nullable=True)
3369 # TODO: dan: rename column to last_merge_target_rev
3368 # TODO: dan: rename column to last_merge_target_rev
3370 _last_merge_target_rev = Column(
3369 _last_merge_target_rev = Column(
3371 'last_merge_other_rev', String(40), nullable=True)
3370 'last_merge_other_rev', String(40), nullable=True)
3372 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3371 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3373 merge_rev = Column('merge_rev', String(40), nullable=True)
3372 merge_rev = Column('merge_rev', String(40), nullable=True)
3374
3373
3375 reviewer_data = Column(
3374 reviewer_data = Column(
3376 'reviewer_data_json', MutationObj.as_mutable(
3375 'reviewer_data_json', MutationObj.as_mutable(
3377 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3376 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3378
3377
3379 @property
3378 @property
3380 def reviewer_data_json(self):
3379 def reviewer_data_json(self):
3381 return json.dumps(self.reviewer_data)
3380 return json.dumps(self.reviewer_data)
3382
3381
3383 @hybrid_property
3382 @hybrid_property
3384 def description_safe(self):
3383 def description_safe(self):
3385 from rhodecode.lib import helpers as h
3384 from rhodecode.lib import helpers as h
3386 return h.escape(self.description)
3385 return h.escape(self.description)
3387
3386
3388 @hybrid_property
3387 @hybrid_property
3389 def revisions(self):
3388 def revisions(self):
3390 return self._revisions.split(':') if self._revisions else []
3389 return self._revisions.split(':') if self._revisions else []
3391
3390
3392 @revisions.setter
3391 @revisions.setter
3393 def revisions(self, val):
3392 def revisions(self, val):
3394 self._revisions = ':'.join(val)
3393 self._revisions = ':'.join(val)
3395
3394
3396 @hybrid_property
3395 @hybrid_property
3397 def last_merge_status(self):
3396 def last_merge_status(self):
3398 return safe_int(self._last_merge_status)
3397 return safe_int(self._last_merge_status)
3399
3398
3400 @last_merge_status.setter
3399 @last_merge_status.setter
3401 def last_merge_status(self, val):
3400 def last_merge_status(self, val):
3402 self._last_merge_status = val
3401 self._last_merge_status = val
3403
3402
3404 @declared_attr
3403 @declared_attr
3405 def author(cls):
3404 def author(cls):
3406 return relationship('User', lazy='joined')
3405 return relationship('User', lazy='joined')
3407
3406
3408 @declared_attr
3407 @declared_attr
3409 def source_repo(cls):
3408 def source_repo(cls):
3410 return relationship(
3409 return relationship(
3411 'Repository',
3410 'Repository',
3412 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3411 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3413
3412
3414 @property
3413 @property
3415 def source_ref_parts(self):
3414 def source_ref_parts(self):
3416 return self.unicode_to_reference(self.source_ref)
3415 return self.unicode_to_reference(self.source_ref)
3417
3416
3418 @declared_attr
3417 @declared_attr
3419 def target_repo(cls):
3418 def target_repo(cls):
3420 return relationship(
3419 return relationship(
3421 'Repository',
3420 'Repository',
3422 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3421 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3423
3422
3424 @property
3423 @property
3425 def target_ref_parts(self):
3424 def target_ref_parts(self):
3426 return self.unicode_to_reference(self.target_ref)
3425 return self.unicode_to_reference(self.target_ref)
3427
3426
3428 @property
3427 @property
3429 def shadow_merge_ref(self):
3428 def shadow_merge_ref(self):
3430 return self.unicode_to_reference(self._shadow_merge_ref)
3429 return self.unicode_to_reference(self._shadow_merge_ref)
3431
3430
3432 @shadow_merge_ref.setter
3431 @shadow_merge_ref.setter
3433 def shadow_merge_ref(self, ref):
3432 def shadow_merge_ref(self, ref):
3434 self._shadow_merge_ref = self.reference_to_unicode(ref)
3433 self._shadow_merge_ref = self.reference_to_unicode(ref)
3435
3434
3436 def unicode_to_reference(self, raw):
3435 def unicode_to_reference(self, raw):
3437 """
3436 """
3438 Convert a unicode (or string) to a reference object.
3437 Convert a unicode (or string) to a reference object.
3439 If unicode evaluates to False it returns None.
3438 If unicode evaluates to False it returns None.
3440 """
3439 """
3441 if raw:
3440 if raw:
3442 refs = raw.split(':')
3441 refs = raw.split(':')
3443 return Reference(*refs)
3442 return Reference(*refs)
3444 else:
3443 else:
3445 return None
3444 return None
3446
3445
3447 def reference_to_unicode(self, ref):
3446 def reference_to_unicode(self, ref):
3448 """
3447 """
3449 Convert a reference object to unicode.
3448 Convert a reference object to unicode.
3450 If reference is None it returns None.
3449 If reference is None it returns None.
3451 """
3450 """
3452 if ref:
3451 if ref:
3453 return u':'.join(ref)
3452 return u':'.join(ref)
3454 else:
3453 else:
3455 return None
3454 return None
3456
3455
3457 def get_api_data(self, with_merge_state=True):
3456 def get_api_data(self, with_merge_state=True):
3458 from rhodecode.model.pull_request import PullRequestModel
3457 from rhodecode.model.pull_request import PullRequestModel
3459
3458
3460 pull_request = self
3459 pull_request = self
3461 if with_merge_state:
3460 if with_merge_state:
3462 merge_status = PullRequestModel().merge_status(pull_request)
3461 merge_status = PullRequestModel().merge_status(pull_request)
3463 merge_state = {
3462 merge_state = {
3464 'status': merge_status[0],
3463 'status': merge_status[0],
3465 'message': safe_unicode(merge_status[1]),
3464 'message': safe_unicode(merge_status[1]),
3466 }
3465 }
3467 else:
3466 else:
3468 merge_state = {'status': 'not_available',
3467 merge_state = {'status': 'not_available',
3469 'message': 'not_available'}
3468 'message': 'not_available'}
3470
3469
3471 merge_data = {
3470 merge_data = {
3472 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3471 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3473 'reference': (
3472 'reference': (
3474 pull_request.shadow_merge_ref._asdict()
3473 pull_request.shadow_merge_ref._asdict()
3475 if pull_request.shadow_merge_ref else None),
3474 if pull_request.shadow_merge_ref else None),
3476 }
3475 }
3477
3476
3478 data = {
3477 data = {
3479 'pull_request_id': pull_request.pull_request_id,
3478 'pull_request_id': pull_request.pull_request_id,
3480 'url': PullRequestModel().get_url(pull_request),
3479 'url': PullRequestModel().get_url(pull_request),
3481 'title': pull_request.title,
3480 'title': pull_request.title,
3482 'description': pull_request.description,
3481 'description': pull_request.description,
3483 'status': pull_request.status,
3482 'status': pull_request.status,
3484 'created_on': pull_request.created_on,
3483 'created_on': pull_request.created_on,
3485 'updated_on': pull_request.updated_on,
3484 'updated_on': pull_request.updated_on,
3486 'commit_ids': pull_request.revisions,
3485 'commit_ids': pull_request.revisions,
3487 'review_status': pull_request.calculated_review_status(),
3486 'review_status': pull_request.calculated_review_status(),
3488 'mergeable': merge_state,
3487 'mergeable': merge_state,
3489 'source': {
3488 'source': {
3490 'clone_url': pull_request.source_repo.clone_url(),
3489 'clone_url': pull_request.source_repo.clone_url(),
3491 'repository': pull_request.source_repo.repo_name,
3490 'repository': pull_request.source_repo.repo_name,
3492 'reference': {
3491 'reference': {
3493 'name': pull_request.source_ref_parts.name,
3492 'name': pull_request.source_ref_parts.name,
3494 'type': pull_request.source_ref_parts.type,
3493 'type': pull_request.source_ref_parts.type,
3495 'commit_id': pull_request.source_ref_parts.commit_id,
3494 'commit_id': pull_request.source_ref_parts.commit_id,
3496 },
3495 },
3497 },
3496 },
3498 'target': {
3497 'target': {
3499 'clone_url': pull_request.target_repo.clone_url(),
3498 'clone_url': pull_request.target_repo.clone_url(),
3500 'repository': pull_request.target_repo.repo_name,
3499 'repository': pull_request.target_repo.repo_name,
3501 'reference': {
3500 'reference': {
3502 'name': pull_request.target_ref_parts.name,
3501 'name': pull_request.target_ref_parts.name,
3503 'type': pull_request.target_ref_parts.type,
3502 'type': pull_request.target_ref_parts.type,
3504 'commit_id': pull_request.target_ref_parts.commit_id,
3503 'commit_id': pull_request.target_ref_parts.commit_id,
3505 },
3504 },
3506 },
3505 },
3507 'merge': merge_data,
3506 'merge': merge_data,
3508 'author': pull_request.author.get_api_data(include_secrets=False,
3507 'author': pull_request.author.get_api_data(include_secrets=False,
3509 details='basic'),
3508 details='basic'),
3510 'reviewers': [
3509 'reviewers': [
3511 {
3510 {
3512 'user': reviewer.get_api_data(include_secrets=False,
3511 'user': reviewer.get_api_data(include_secrets=False,
3513 details='basic'),
3512 details='basic'),
3514 'reasons': reasons,
3513 'reasons': reasons,
3515 'review_status': st[0][1].status if st else 'not_reviewed',
3514 'review_status': st[0][1].status if st else 'not_reviewed',
3516 }
3515 }
3517 for reviewer, reasons, mandatory, st in
3516 for reviewer, reasons, mandatory, st in
3518 pull_request.reviewers_statuses()
3517 pull_request.reviewers_statuses()
3519 ]
3518 ]
3520 }
3519 }
3521
3520
3522 return data
3521 return data
3523
3522
3524
3523
3525 class PullRequest(Base, _PullRequestBase):
3524 class PullRequest(Base, _PullRequestBase):
3526 __tablename__ = 'pull_requests'
3525 __tablename__ = 'pull_requests'
3527 __table_args__ = (
3526 __table_args__ = (
3528 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3527 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3529 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3528 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3530 )
3529 )
3531
3530
3532 pull_request_id = Column(
3531 pull_request_id = Column(
3533 'pull_request_id', Integer(), nullable=False, primary_key=True)
3532 'pull_request_id', Integer(), nullable=False, primary_key=True)
3534
3533
3535 def __repr__(self):
3534 def __repr__(self):
3536 if self.pull_request_id:
3535 if self.pull_request_id:
3537 return '<DB:PullRequest #%s>' % self.pull_request_id
3536 return '<DB:PullRequest #%s>' % self.pull_request_id
3538 else:
3537 else:
3539 return '<DB:PullRequest at %#x>' % id(self)
3538 return '<DB:PullRequest at %#x>' % id(self)
3540
3539
3541 reviewers = relationship('PullRequestReviewers',
3540 reviewers = relationship('PullRequestReviewers',
3542 cascade="all, delete, delete-orphan")
3541 cascade="all, delete, delete-orphan")
3543 statuses = relationship('ChangesetStatus',
3542 statuses = relationship('ChangesetStatus',
3544 cascade="all, delete, delete-orphan")
3543 cascade="all, delete, delete-orphan")
3545 comments = relationship('ChangesetComment',
3544 comments = relationship('ChangesetComment',
3546 cascade="all, delete, delete-orphan")
3545 cascade="all, delete, delete-orphan")
3547 versions = relationship('PullRequestVersion',
3546 versions = relationship('PullRequestVersion',
3548 cascade="all, delete, delete-orphan",
3547 cascade="all, delete, delete-orphan",
3549 lazy='dynamic')
3548 lazy='dynamic')
3550
3549
3551 @classmethod
3550 @classmethod
3552 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3551 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3553 internal_methods=None):
3552 internal_methods=None):
3554
3553
3555 class PullRequestDisplay(object):
3554 class PullRequestDisplay(object):
3556 """
3555 """
3557 Special object wrapper for showing PullRequest data via Versions
3556 Special object wrapper for showing PullRequest data via Versions
3558 It mimics PR object as close as possible. This is read only object
3557 It mimics PR object as close as possible. This is read only object
3559 just for display
3558 just for display
3560 """
3559 """
3561
3560
3562 def __init__(self, attrs, internal=None):
3561 def __init__(self, attrs, internal=None):
3563 self.attrs = attrs
3562 self.attrs = attrs
3564 # internal have priority over the given ones via attrs
3563 # internal have priority over the given ones via attrs
3565 self.internal = internal or ['versions']
3564 self.internal = internal or ['versions']
3566
3565
3567 def __getattr__(self, item):
3566 def __getattr__(self, item):
3568 if item in self.internal:
3567 if item in self.internal:
3569 return getattr(self, item)
3568 return getattr(self, item)
3570 try:
3569 try:
3571 return self.attrs[item]
3570 return self.attrs[item]
3572 except KeyError:
3571 except KeyError:
3573 raise AttributeError(
3572 raise AttributeError(
3574 '%s object has no attribute %s' % (self, item))
3573 '%s object has no attribute %s' % (self, item))
3575
3574
3576 def __repr__(self):
3575 def __repr__(self):
3577 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3576 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3578
3577
3579 def versions(self):
3578 def versions(self):
3580 return pull_request_obj.versions.order_by(
3579 return pull_request_obj.versions.order_by(
3581 PullRequestVersion.pull_request_version_id).all()
3580 PullRequestVersion.pull_request_version_id).all()
3582
3581
3583 def is_closed(self):
3582 def is_closed(self):
3584 return pull_request_obj.is_closed()
3583 return pull_request_obj.is_closed()
3585
3584
3586 @property
3585 @property
3587 def pull_request_version_id(self):
3586 def pull_request_version_id(self):
3588 return getattr(pull_request_obj, 'pull_request_version_id', None)
3587 return getattr(pull_request_obj, 'pull_request_version_id', None)
3589
3588
3590 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3589 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3591
3590
3592 attrs.author = StrictAttributeDict(
3591 attrs.author = StrictAttributeDict(
3593 pull_request_obj.author.get_api_data())
3592 pull_request_obj.author.get_api_data())
3594 if pull_request_obj.target_repo:
3593 if pull_request_obj.target_repo:
3595 attrs.target_repo = StrictAttributeDict(
3594 attrs.target_repo = StrictAttributeDict(
3596 pull_request_obj.target_repo.get_api_data())
3595 pull_request_obj.target_repo.get_api_data())
3597 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3596 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3598
3597
3599 if pull_request_obj.source_repo:
3598 if pull_request_obj.source_repo:
3600 attrs.source_repo = StrictAttributeDict(
3599 attrs.source_repo = StrictAttributeDict(
3601 pull_request_obj.source_repo.get_api_data())
3600 pull_request_obj.source_repo.get_api_data())
3602 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3601 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3603
3602
3604 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3603 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3605 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3604 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3606 attrs.revisions = pull_request_obj.revisions
3605 attrs.revisions = pull_request_obj.revisions
3607
3606
3608 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3607 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3609 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3608 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3610 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3609 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3611
3610
3612 return PullRequestDisplay(attrs, internal=internal_methods)
3611 return PullRequestDisplay(attrs, internal=internal_methods)
3613
3612
3614 def is_closed(self):
3613 def is_closed(self):
3615 return self.status == self.STATUS_CLOSED
3614 return self.status == self.STATUS_CLOSED
3616
3615
3617 def __json__(self):
3616 def __json__(self):
3618 return {
3617 return {
3619 'revisions': self.revisions,
3618 'revisions': self.revisions,
3620 }
3619 }
3621
3620
3622 def calculated_review_status(self):
3621 def calculated_review_status(self):
3623 from rhodecode.model.changeset_status import ChangesetStatusModel
3622 from rhodecode.model.changeset_status import ChangesetStatusModel
3624 return ChangesetStatusModel().calculated_review_status(self)
3623 return ChangesetStatusModel().calculated_review_status(self)
3625
3624
3626 def reviewers_statuses(self):
3625 def reviewers_statuses(self):
3627 from rhodecode.model.changeset_status import ChangesetStatusModel
3626 from rhodecode.model.changeset_status import ChangesetStatusModel
3628 return ChangesetStatusModel().reviewers_statuses(self)
3627 return ChangesetStatusModel().reviewers_statuses(self)
3629
3628
3630 @property
3629 @property
3631 def workspace_id(self):
3630 def workspace_id(self):
3632 from rhodecode.model.pull_request import PullRequestModel
3631 from rhodecode.model.pull_request import PullRequestModel
3633 return PullRequestModel()._workspace_id(self)
3632 return PullRequestModel()._workspace_id(self)
3634
3633
3635 def get_shadow_repo(self):
3634 def get_shadow_repo(self):
3636 workspace_id = self.workspace_id
3635 workspace_id = self.workspace_id
3637 vcs_obj = self.target_repo.scm_instance()
3636 vcs_obj = self.target_repo.scm_instance()
3638 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3637 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3639 workspace_id)
3638 workspace_id)
3640 return vcs_obj._get_shadow_instance(shadow_repository_path)
3639 return vcs_obj._get_shadow_instance(shadow_repository_path)
3641
3640
3642
3641
3643 class PullRequestVersion(Base, _PullRequestBase):
3642 class PullRequestVersion(Base, _PullRequestBase):
3644 __tablename__ = 'pull_request_versions'
3643 __tablename__ = 'pull_request_versions'
3645 __table_args__ = (
3644 __table_args__ = (
3646 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3645 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3647 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3646 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3648 )
3647 )
3649
3648
3650 pull_request_version_id = Column(
3649 pull_request_version_id = Column(
3651 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3650 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3652 pull_request_id = Column(
3651 pull_request_id = Column(
3653 'pull_request_id', Integer(),
3652 'pull_request_id', Integer(),
3654 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3653 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3655 pull_request = relationship('PullRequest')
3654 pull_request = relationship('PullRequest')
3656
3655
3657 def __repr__(self):
3656 def __repr__(self):
3658 if self.pull_request_version_id:
3657 if self.pull_request_version_id:
3659 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3658 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3660 else:
3659 else:
3661 return '<DB:PullRequestVersion at %#x>' % id(self)
3660 return '<DB:PullRequestVersion at %#x>' % id(self)
3662
3661
3663 @property
3662 @property
3664 def reviewers(self):
3663 def reviewers(self):
3665 return self.pull_request.reviewers
3664 return self.pull_request.reviewers
3666
3665
3667 @property
3666 @property
3668 def versions(self):
3667 def versions(self):
3669 return self.pull_request.versions
3668 return self.pull_request.versions
3670
3669
3671 def is_closed(self):
3670 def is_closed(self):
3672 # calculate from original
3671 # calculate from original
3673 return self.pull_request.status == self.STATUS_CLOSED
3672 return self.pull_request.status == self.STATUS_CLOSED
3674
3673
3675 def calculated_review_status(self):
3674 def calculated_review_status(self):
3676 return self.pull_request.calculated_review_status()
3675 return self.pull_request.calculated_review_status()
3677
3676
3678 def reviewers_statuses(self):
3677 def reviewers_statuses(self):
3679 return self.pull_request.reviewers_statuses()
3678 return self.pull_request.reviewers_statuses()
3680
3679
3681
3680
3682 class PullRequestReviewers(Base, BaseModel):
3681 class PullRequestReviewers(Base, BaseModel):
3683 __tablename__ = 'pull_request_reviewers'
3682 __tablename__ = 'pull_request_reviewers'
3684 __table_args__ = (
3683 __table_args__ = (
3685 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3684 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3686 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3685 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3687 )
3686 )
3688
3687
3689 @hybrid_property
3688 @hybrid_property
3690 def reasons(self):
3689 def reasons(self):
3691 if not self._reasons:
3690 if not self._reasons:
3692 return []
3691 return []
3693 return self._reasons
3692 return self._reasons
3694
3693
3695 @reasons.setter
3694 @reasons.setter
3696 def reasons(self, val):
3695 def reasons(self, val):
3697 val = val or []
3696 val = val or []
3698 if any(not isinstance(x, basestring) for x in val):
3697 if any(not isinstance(x, basestring) for x in val):
3699 raise Exception('invalid reasons type, must be list of strings')
3698 raise Exception('invalid reasons type, must be list of strings')
3700 self._reasons = val
3699 self._reasons = val
3701
3700
3702 pull_requests_reviewers_id = Column(
3701 pull_requests_reviewers_id = Column(
3703 'pull_requests_reviewers_id', Integer(), nullable=False,
3702 'pull_requests_reviewers_id', Integer(), nullable=False,
3704 primary_key=True)
3703 primary_key=True)
3705 pull_request_id = Column(
3704 pull_request_id = Column(
3706 "pull_request_id", Integer(),
3705 "pull_request_id", Integer(),
3707 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3706 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3708 user_id = Column(
3707 user_id = Column(
3709 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3708 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3710 _reasons = Column(
3709 _reasons = Column(
3711 'reason', MutationList.as_mutable(
3710 'reason', MutationList.as_mutable(
3712 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3711 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3713 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3712 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3714 user = relationship('User')
3713 user = relationship('User')
3715 pull_request = relationship('PullRequest')
3714 pull_request = relationship('PullRequest')
3716
3715
3717
3716
3718 class Notification(Base, BaseModel):
3717 class Notification(Base, BaseModel):
3719 __tablename__ = 'notifications'
3718 __tablename__ = 'notifications'
3720 __table_args__ = (
3719 __table_args__ = (
3721 Index('notification_type_idx', 'type'),
3720 Index('notification_type_idx', 'type'),
3722 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3721 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3723 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3722 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3724 )
3723 )
3725
3724
3726 TYPE_CHANGESET_COMMENT = u'cs_comment'
3725 TYPE_CHANGESET_COMMENT = u'cs_comment'
3727 TYPE_MESSAGE = u'message'
3726 TYPE_MESSAGE = u'message'
3728 TYPE_MENTION = u'mention'
3727 TYPE_MENTION = u'mention'
3729 TYPE_REGISTRATION = u'registration'
3728 TYPE_REGISTRATION = u'registration'
3730 TYPE_PULL_REQUEST = u'pull_request'
3729 TYPE_PULL_REQUEST = u'pull_request'
3731 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3730 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3732
3731
3733 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3732 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3734 subject = Column('subject', Unicode(512), nullable=True)
3733 subject = Column('subject', Unicode(512), nullable=True)
3735 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3734 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3736 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3735 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3737 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3736 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3738 type_ = Column('type', Unicode(255))
3737 type_ = Column('type', Unicode(255))
3739
3738
3740 created_by_user = relationship('User')
3739 created_by_user = relationship('User')
3741 notifications_to_users = relationship('UserNotification', lazy='joined',
3740 notifications_to_users = relationship('UserNotification', lazy='joined',
3742 cascade="all, delete, delete-orphan")
3741 cascade="all, delete, delete-orphan")
3743
3742
3744 @property
3743 @property
3745 def recipients(self):
3744 def recipients(self):
3746 return [x.user for x in UserNotification.query()\
3745 return [x.user for x in UserNotification.query()\
3747 .filter(UserNotification.notification == self)\
3746 .filter(UserNotification.notification == self)\
3748 .order_by(UserNotification.user_id.asc()).all()]
3747 .order_by(UserNotification.user_id.asc()).all()]
3749
3748
3750 @classmethod
3749 @classmethod
3751 def create(cls, created_by, subject, body, recipients, type_=None):
3750 def create(cls, created_by, subject, body, recipients, type_=None):
3752 if type_ is None:
3751 if type_ is None:
3753 type_ = Notification.TYPE_MESSAGE
3752 type_ = Notification.TYPE_MESSAGE
3754
3753
3755 notification = cls()
3754 notification = cls()
3756 notification.created_by_user = created_by
3755 notification.created_by_user = created_by
3757 notification.subject = subject
3756 notification.subject = subject
3758 notification.body = body
3757 notification.body = body
3759 notification.type_ = type_
3758 notification.type_ = type_
3760 notification.created_on = datetime.datetime.now()
3759 notification.created_on = datetime.datetime.now()
3761
3760
3762 for u in recipients:
3761 for u in recipients:
3763 assoc = UserNotification()
3762 assoc = UserNotification()
3764 assoc.notification = notification
3763 assoc.notification = notification
3765
3764
3766 # if created_by is inside recipients mark his notification
3765 # if created_by is inside recipients mark his notification
3767 # as read
3766 # as read
3768 if u.user_id == created_by.user_id:
3767 if u.user_id == created_by.user_id:
3769 assoc.read = True
3768 assoc.read = True
3770
3769
3771 u.notifications.append(assoc)
3770 u.notifications.append(assoc)
3772 Session().add(notification)
3771 Session().add(notification)
3773
3772
3774 return notification
3773 return notification
3775
3774
3776
3775
3777 class UserNotification(Base, BaseModel):
3776 class UserNotification(Base, BaseModel):
3778 __tablename__ = 'user_to_notification'
3777 __tablename__ = 'user_to_notification'
3779 __table_args__ = (
3778 __table_args__ = (
3780 UniqueConstraint('user_id', 'notification_id'),
3779 UniqueConstraint('user_id', 'notification_id'),
3781 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3780 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3782 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3781 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3783 )
3782 )
3784 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3783 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3785 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3784 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3786 read = Column('read', Boolean, default=False)
3785 read = Column('read', Boolean, default=False)
3787 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3786 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3788
3787
3789 user = relationship('User', lazy="joined")
3788 user = relationship('User', lazy="joined")
3790 notification = relationship('Notification', lazy="joined",
3789 notification = relationship('Notification', lazy="joined",
3791 order_by=lambda: Notification.created_on.desc(),)
3790 order_by=lambda: Notification.created_on.desc(),)
3792
3791
3793 def mark_as_read(self):
3792 def mark_as_read(self):
3794 self.read = True
3793 self.read = True
3795 Session().add(self)
3794 Session().add(self)
3796
3795
3797
3796
3798 class Gist(Base, BaseModel):
3797 class Gist(Base, BaseModel):
3799 __tablename__ = 'gists'
3798 __tablename__ = 'gists'
3800 __table_args__ = (
3799 __table_args__ = (
3801 Index('g_gist_access_id_idx', 'gist_access_id'),
3800 Index('g_gist_access_id_idx', 'gist_access_id'),
3802 Index('g_created_on_idx', 'created_on'),
3801 Index('g_created_on_idx', 'created_on'),
3803 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3802 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3804 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3803 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3805 )
3804 )
3806 GIST_PUBLIC = u'public'
3805 GIST_PUBLIC = u'public'
3807 GIST_PRIVATE = u'private'
3806 GIST_PRIVATE = u'private'
3808 DEFAULT_FILENAME = u'gistfile1.txt'
3807 DEFAULT_FILENAME = u'gistfile1.txt'
3809
3808
3810 ACL_LEVEL_PUBLIC = u'acl_public'
3809 ACL_LEVEL_PUBLIC = u'acl_public'
3811 ACL_LEVEL_PRIVATE = u'acl_private'
3810 ACL_LEVEL_PRIVATE = u'acl_private'
3812
3811
3813 gist_id = Column('gist_id', Integer(), primary_key=True)
3812 gist_id = Column('gist_id', Integer(), primary_key=True)
3814 gist_access_id = Column('gist_access_id', Unicode(250))
3813 gist_access_id = Column('gist_access_id', Unicode(250))
3815 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3814 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3816 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3815 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3817 gist_expires = Column('gist_expires', Float(53), nullable=False)
3816 gist_expires = Column('gist_expires', Float(53), nullable=False)
3818 gist_type = Column('gist_type', Unicode(128), nullable=False)
3817 gist_type = Column('gist_type', Unicode(128), nullable=False)
3819 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3818 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3820 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3819 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3821 acl_level = Column('acl_level', Unicode(128), nullable=True)
3820 acl_level = Column('acl_level', Unicode(128), nullable=True)
3822
3821
3823 owner = relationship('User')
3822 owner = relationship('User')
3824
3823
3825 def __repr__(self):
3824 def __repr__(self):
3826 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3825 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3827
3826
3828 @hybrid_property
3827 @hybrid_property
3829 def description_safe(self):
3828 def description_safe(self):
3830 from rhodecode.lib import helpers as h
3829 from rhodecode.lib import helpers as h
3831 return h.escape(self.gist_description)
3830 return h.escape(self.gist_description)
3832
3831
3833 @classmethod
3832 @classmethod
3834 def get_or_404(cls, id_):
3833 def get_or_404(cls, id_):
3835 from pyramid.httpexceptions import HTTPNotFound
3834 from pyramid.httpexceptions import HTTPNotFound
3836
3835
3837 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3836 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3838 if not res:
3837 if not res:
3839 raise HTTPNotFound()
3838 raise HTTPNotFound()
3840 return res
3839 return res
3841
3840
3842 @classmethod
3841 @classmethod
3843 def get_by_access_id(cls, gist_access_id):
3842 def get_by_access_id(cls, gist_access_id):
3844 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3843 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3845
3844
3846 def gist_url(self):
3845 def gist_url(self):
3847 from rhodecode.model.gist import GistModel
3846 from rhodecode.model.gist import GistModel
3848 return GistModel().get_url(self)
3847 return GistModel().get_url(self)
3849
3848
3850 @classmethod
3849 @classmethod
3851 def base_path(cls):
3850 def base_path(cls):
3852 """
3851 """
3853 Returns base path when all gists are stored
3852 Returns base path when all gists are stored
3854
3853
3855 :param cls:
3854 :param cls:
3856 """
3855 """
3857 from rhodecode.model.gist import GIST_STORE_LOC
3856 from rhodecode.model.gist import GIST_STORE_LOC
3858 q = Session().query(RhodeCodeUi)\
3857 q = Session().query(RhodeCodeUi)\
3859 .filter(RhodeCodeUi.ui_key == URL_SEP)
3858 .filter(RhodeCodeUi.ui_key == URL_SEP)
3860 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3859 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3861 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3860 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3862
3861
3863 def get_api_data(self):
3862 def get_api_data(self):
3864 """
3863 """
3865 Common function for generating gist related data for API
3864 Common function for generating gist related data for API
3866 """
3865 """
3867 gist = self
3866 gist = self
3868 data = {
3867 data = {
3869 'gist_id': gist.gist_id,
3868 'gist_id': gist.gist_id,
3870 'type': gist.gist_type,
3869 'type': gist.gist_type,
3871 'access_id': gist.gist_access_id,
3870 'access_id': gist.gist_access_id,
3872 'description': gist.gist_description,
3871 'description': gist.gist_description,
3873 'url': gist.gist_url(),
3872 'url': gist.gist_url(),
3874 'expires': gist.gist_expires,
3873 'expires': gist.gist_expires,
3875 'created_on': gist.created_on,
3874 'created_on': gist.created_on,
3876 'modified_at': gist.modified_at,
3875 'modified_at': gist.modified_at,
3877 'content': None,
3876 'content': None,
3878 'acl_level': gist.acl_level,
3877 'acl_level': gist.acl_level,
3879 }
3878 }
3880 return data
3879 return data
3881
3880
3882 def __json__(self):
3881 def __json__(self):
3883 data = dict(
3882 data = dict(
3884 )
3883 )
3885 data.update(self.get_api_data())
3884 data.update(self.get_api_data())
3886 return data
3885 return data
3887 # SCM functions
3886 # SCM functions
3888
3887
3889 def scm_instance(self, **kwargs):
3888 def scm_instance(self, **kwargs):
3890 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3889 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3891 return get_vcs_instance(
3890 return get_vcs_instance(
3892 repo_path=safe_str(full_repo_path), create=False)
3891 repo_path=safe_str(full_repo_path), create=False)
3893
3892
3894
3893
3895 class ExternalIdentity(Base, BaseModel):
3894 class ExternalIdentity(Base, BaseModel):
3896 __tablename__ = 'external_identities'
3895 __tablename__ = 'external_identities'
3897 __table_args__ = (
3896 __table_args__ = (
3898 Index('local_user_id_idx', 'local_user_id'),
3897 Index('local_user_id_idx', 'local_user_id'),
3899 Index('external_id_idx', 'external_id'),
3898 Index('external_id_idx', 'external_id'),
3900 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3899 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3901 'mysql_charset': 'utf8'})
3900 'mysql_charset': 'utf8'})
3902
3901
3903 external_id = Column('external_id', Unicode(255), default=u'',
3902 external_id = Column('external_id', Unicode(255), default=u'',
3904 primary_key=True)
3903 primary_key=True)
3905 external_username = Column('external_username', Unicode(1024), default=u'')
3904 external_username = Column('external_username', Unicode(1024), default=u'')
3906 local_user_id = Column('local_user_id', Integer(),
3905 local_user_id = Column('local_user_id', Integer(),
3907 ForeignKey('users.user_id'), primary_key=True)
3906 ForeignKey('users.user_id'), primary_key=True)
3908 provider_name = Column('provider_name', Unicode(255), default=u'',
3907 provider_name = Column('provider_name', Unicode(255), default=u'',
3909 primary_key=True)
3908 primary_key=True)
3910 access_token = Column('access_token', String(1024), default=u'')
3909 access_token = Column('access_token', String(1024), default=u'')
3911 alt_token = Column('alt_token', String(1024), default=u'')
3910 alt_token = Column('alt_token', String(1024), default=u'')
3912 token_secret = Column('token_secret', String(1024), default=u'')
3911 token_secret = Column('token_secret', String(1024), default=u'')
3913
3912
3914 @classmethod
3913 @classmethod
3915 def by_external_id_and_provider(cls, external_id, provider_name,
3914 def by_external_id_and_provider(cls, external_id, provider_name,
3916 local_user_id=None):
3915 local_user_id=None):
3917 """
3916 """
3918 Returns ExternalIdentity instance based on search params
3917 Returns ExternalIdentity instance based on search params
3919
3918
3920 :param external_id:
3919 :param external_id:
3921 :param provider_name:
3920 :param provider_name:
3922 :return: ExternalIdentity
3921 :return: ExternalIdentity
3923 """
3922 """
3924 query = cls.query()
3923 query = cls.query()
3925 query = query.filter(cls.external_id == external_id)
3924 query = query.filter(cls.external_id == external_id)
3926 query = query.filter(cls.provider_name == provider_name)
3925 query = query.filter(cls.provider_name == provider_name)
3927 if local_user_id:
3926 if local_user_id:
3928 query = query.filter(cls.local_user_id == local_user_id)
3927 query = query.filter(cls.local_user_id == local_user_id)
3929 return query.first()
3928 return query.first()
3930
3929
3931 @classmethod
3930 @classmethod
3932 def user_by_external_id_and_provider(cls, external_id, provider_name):
3931 def user_by_external_id_and_provider(cls, external_id, provider_name):
3933 """
3932 """
3934 Returns User instance based on search params
3933 Returns User instance based on search params
3935
3934
3936 :param external_id:
3935 :param external_id:
3937 :param provider_name:
3936 :param provider_name:
3938 :return: User
3937 :return: User
3939 """
3938 """
3940 query = User.query()
3939 query = User.query()
3941 query = query.filter(cls.external_id == external_id)
3940 query = query.filter(cls.external_id == external_id)
3942 query = query.filter(cls.provider_name == provider_name)
3941 query = query.filter(cls.provider_name == provider_name)
3943 query = query.filter(User.user_id == cls.local_user_id)
3942 query = query.filter(User.user_id == cls.local_user_id)
3944 return query.first()
3943 return query.first()
3945
3944
3946 @classmethod
3945 @classmethod
3947 def by_local_user_id(cls, local_user_id):
3946 def by_local_user_id(cls, local_user_id):
3948 """
3947 """
3949 Returns all tokens for user
3948 Returns all tokens for user
3950
3949
3951 :param local_user_id:
3950 :param local_user_id:
3952 :return: ExternalIdentity
3951 :return: ExternalIdentity
3953 """
3952 """
3954 query = cls.query()
3953 query = cls.query()
3955 query = query.filter(cls.local_user_id == local_user_id)
3954 query = query.filter(cls.local_user_id == local_user_id)
3956 return query
3955 return query
3957
3956
3958
3957
3959 class Integration(Base, BaseModel):
3958 class Integration(Base, BaseModel):
3960 __tablename__ = 'integrations'
3959 __tablename__ = 'integrations'
3961 __table_args__ = (
3960 __table_args__ = (
3962 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3961 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3963 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3962 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3964 )
3963 )
3965
3964
3966 integration_id = Column('integration_id', Integer(), primary_key=True)
3965 integration_id = Column('integration_id', Integer(), primary_key=True)
3967 integration_type = Column('integration_type', String(255))
3966 integration_type = Column('integration_type', String(255))
3968 enabled = Column('enabled', Boolean(), nullable=False)
3967 enabled = Column('enabled', Boolean(), nullable=False)
3969 name = Column('name', String(255), nullable=False)
3968 name = Column('name', String(255), nullable=False)
3970 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3969 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3971 default=False)
3970 default=False)
3972
3971
3973 settings = Column(
3972 settings = Column(
3974 'settings_json', MutationObj.as_mutable(
3973 'settings_json', MutationObj.as_mutable(
3975 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3974 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3976 repo_id = Column(
3975 repo_id = Column(
3977 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3976 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3978 nullable=True, unique=None, default=None)
3977 nullable=True, unique=None, default=None)
3979 repo = relationship('Repository', lazy='joined')
3978 repo = relationship('Repository', lazy='joined')
3980
3979
3981 repo_group_id = Column(
3980 repo_group_id = Column(
3982 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3981 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3983 nullable=True, unique=None, default=None)
3982 nullable=True, unique=None, default=None)
3984 repo_group = relationship('RepoGroup', lazy='joined')
3983 repo_group = relationship('RepoGroup', lazy='joined')
3985
3984
3986 @property
3985 @property
3987 def scope(self):
3986 def scope(self):
3988 if self.repo:
3987 if self.repo:
3989 return repr(self.repo)
3988 return repr(self.repo)
3990 if self.repo_group:
3989 if self.repo_group:
3991 if self.child_repos_only:
3990 if self.child_repos_only:
3992 return repr(self.repo_group) + ' (child repos only)'
3991 return repr(self.repo_group) + ' (child repos only)'
3993 else:
3992 else:
3994 return repr(self.repo_group) + ' (recursive)'
3993 return repr(self.repo_group) + ' (recursive)'
3995 if self.child_repos_only:
3994 if self.child_repos_only:
3996 return 'root_repos'
3995 return 'root_repos'
3997 return 'global'
3996 return 'global'
3998
3997
3999 def __repr__(self):
3998 def __repr__(self):
4000 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3999 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4001
4000
4002
4001
4003 class RepoReviewRuleUser(Base, BaseModel):
4002 class RepoReviewRuleUser(Base, BaseModel):
4004 __tablename__ = 'repo_review_rules_users'
4003 __tablename__ = 'repo_review_rules_users'
4005 __table_args__ = (
4004 __table_args__ = (
4006 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4005 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4007 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4006 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4008 )
4007 )
4009 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4008 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4010 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4009 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4011 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4010 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4012 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4011 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4013 user = relationship('User')
4012 user = relationship('User')
4014
4013
4015 def rule_data(self):
4014 def rule_data(self):
4016 return {
4015 return {
4017 'mandatory': self.mandatory
4016 'mandatory': self.mandatory
4018 }
4017 }
4019
4018
4020
4019
4021 class RepoReviewRuleUserGroup(Base, BaseModel):
4020 class RepoReviewRuleUserGroup(Base, BaseModel):
4022 __tablename__ = 'repo_review_rules_users_groups'
4021 __tablename__ = 'repo_review_rules_users_groups'
4023 __table_args__ = (
4022 __table_args__ = (
4024 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4023 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4025 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4024 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4026 )
4025 )
4027 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4026 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4028 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4027 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4029 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4028 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4030 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4029 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4031 users_group = relationship('UserGroup')
4030 users_group = relationship('UserGroup')
4032
4031
4033 def rule_data(self):
4032 def rule_data(self):
4034 return {
4033 return {
4035 'mandatory': self.mandatory
4034 'mandatory': self.mandatory
4036 }
4035 }
4037
4036
4038
4037
4039 class RepoReviewRule(Base, BaseModel):
4038 class RepoReviewRule(Base, BaseModel):
4040 __tablename__ = 'repo_review_rules'
4039 __tablename__ = 'repo_review_rules'
4041 __table_args__ = (
4040 __table_args__ = (
4042 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4041 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4043 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4042 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4044 )
4043 )
4045
4044
4046 repo_review_rule_id = Column(
4045 repo_review_rule_id = Column(
4047 'repo_review_rule_id', Integer(), primary_key=True)
4046 'repo_review_rule_id', Integer(), primary_key=True)
4048 repo_id = Column(
4047 repo_id = Column(
4049 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
4048 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
4050 repo = relationship('Repository', backref='review_rules')
4049 repo = relationship('Repository', backref='review_rules')
4051
4050
4052 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4051 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4053 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4052 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4054
4053
4055 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4054 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4056 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4055 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4057 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4056 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4058 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4057 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4059
4058
4060 rule_users = relationship('RepoReviewRuleUser')
4059 rule_users = relationship('RepoReviewRuleUser')
4061 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4060 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4062
4061
4063 @hybrid_property
4062 @hybrid_property
4064 def branch_pattern(self):
4063 def branch_pattern(self):
4065 return self._branch_pattern or '*'
4064 return self._branch_pattern or '*'
4066
4065
4067 def _validate_glob(self, value):
4066 def _validate_glob(self, value):
4068 re.compile('^' + glob2re(value) + '$')
4067 re.compile('^' + glob2re(value) + '$')
4069
4068
4070 @branch_pattern.setter
4069 @branch_pattern.setter
4071 def branch_pattern(self, value):
4070 def branch_pattern(self, value):
4072 self._validate_glob(value)
4071 self._validate_glob(value)
4073 self._branch_pattern = value or '*'
4072 self._branch_pattern = value or '*'
4074
4073
4075 @hybrid_property
4074 @hybrid_property
4076 def file_pattern(self):
4075 def file_pattern(self):
4077 return self._file_pattern or '*'
4076 return self._file_pattern or '*'
4078
4077
4079 @file_pattern.setter
4078 @file_pattern.setter
4080 def file_pattern(self, value):
4079 def file_pattern(self, value):
4081 self._validate_glob(value)
4080 self._validate_glob(value)
4082 self._file_pattern = value or '*'
4081 self._file_pattern = value or '*'
4083
4082
4084 def matches(self, branch, files_changed):
4083 def matches(self, branch, files_changed):
4085 """
4084 """
4086 Check if this review rule matches a branch/files in a pull request
4085 Check if this review rule matches a branch/files in a pull request
4087
4086
4088 :param branch: branch name for the commit
4087 :param branch: branch name for the commit
4089 :param files_changed: list of file paths changed in the pull request
4088 :param files_changed: list of file paths changed in the pull request
4090 """
4089 """
4091
4090
4092 branch = branch or ''
4091 branch = branch or ''
4093 files_changed = files_changed or []
4092 files_changed = files_changed or []
4094
4093
4095 branch_matches = True
4094 branch_matches = True
4096 if branch:
4095 if branch:
4097 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
4096 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
4098 branch_matches = bool(branch_regex.search(branch))
4097 branch_matches = bool(branch_regex.search(branch))
4099
4098
4100 files_matches = True
4099 files_matches = True
4101 if self.file_pattern != '*':
4100 if self.file_pattern != '*':
4102 files_matches = False
4101 files_matches = False
4103 file_regex = re.compile(glob2re(self.file_pattern))
4102 file_regex = re.compile(glob2re(self.file_pattern))
4104 for filename in files_changed:
4103 for filename in files_changed:
4105 if file_regex.search(filename):
4104 if file_regex.search(filename):
4106 files_matches = True
4105 files_matches = True
4107 break
4106 break
4108
4107
4109 return branch_matches and files_matches
4108 return branch_matches and files_matches
4110
4109
4111 @property
4110 @property
4112 def review_users(self):
4111 def review_users(self):
4113 """ Returns the users which this rule applies to """
4112 """ Returns the users which this rule applies to """
4114
4113
4115 users = collections.OrderedDict()
4114 users = collections.OrderedDict()
4116
4115
4117 for rule_user in self.rule_users:
4116 for rule_user in self.rule_users:
4118 if rule_user.user.active:
4117 if rule_user.user.active:
4119 if rule_user.user not in users:
4118 if rule_user.user not in users:
4120 users[rule_user.user.username] = {
4119 users[rule_user.user.username] = {
4121 'user': rule_user.user,
4120 'user': rule_user.user,
4122 'source': 'user',
4121 'source': 'user',
4123 'source_data': {},
4122 'source_data': {},
4124 'data': rule_user.rule_data()
4123 'data': rule_user.rule_data()
4125 }
4124 }
4126
4125
4127 for rule_user_group in self.rule_user_groups:
4126 for rule_user_group in self.rule_user_groups:
4128 source_data = {
4127 source_data = {
4129 'name': rule_user_group.users_group.users_group_name,
4128 'name': rule_user_group.users_group.users_group_name,
4130 'members': len(rule_user_group.users_group.members)
4129 'members': len(rule_user_group.users_group.members)
4131 }
4130 }
4132 for member in rule_user_group.users_group.members:
4131 for member in rule_user_group.users_group.members:
4133 if member.user.active:
4132 if member.user.active:
4134 users[member.user.username] = {
4133 users[member.user.username] = {
4135 'user': member.user,
4134 'user': member.user,
4136 'source': 'user_group',
4135 'source': 'user_group',
4137 'source_data': source_data,
4136 'source_data': source_data,
4138 'data': rule_user_group.rule_data()
4137 'data': rule_user_group.rule_data()
4139 }
4138 }
4140
4139
4141 return users
4140 return users
4142
4141
4143 def __repr__(self):
4142 def __repr__(self):
4144 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4143 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4145 self.repo_review_rule_id, self.repo)
4144 self.repo_review_rule_id, self.repo)
4146
4145
4147
4146
4148 class DbMigrateVersion(Base, BaseModel):
4147 class DbMigrateVersion(Base, BaseModel):
4149 __tablename__ = 'db_migrate_version'
4148 __tablename__ = 'db_migrate_version'
4150 __table_args__ = (
4149 __table_args__ = (
4151 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4150 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4152 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4151 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4153 )
4152 )
4154 repository_id = Column('repository_id', String(250), primary_key=True)
4153 repository_id = Column('repository_id', String(250), primary_key=True)
4155 repository_path = Column('repository_path', Text)
4154 repository_path = Column('repository_path', Text)
4156 version = Column('version', Integer)
4155 version = Column('version', Integer)
4157
4156
4158
4157
4159 class DbSession(Base, BaseModel):
4158 class DbSession(Base, BaseModel):
4160 __tablename__ = 'db_session'
4159 __tablename__ = 'db_session'
4161 __table_args__ = (
4160 __table_args__ = (
4162 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4161 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4163 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4162 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4164 )
4163 )
4165
4164
4166 def __repr__(self):
4165 def __repr__(self):
4167 return '<DB:DbSession({})>'.format(self.id)
4166 return '<DB:DbSession({})>'.format(self.id)
4168
4167
4169 id = Column('id', Integer())
4168 id = Column('id', Integer())
4170 namespace = Column('namespace', String(255), primary_key=True)
4169 namespace = Column('namespace', String(255), primary_key=True)
4171 accessed = Column('accessed', DateTime, nullable=False)
4170 accessed = Column('accessed', DateTime, nullable=False)
4172 created = Column('created', DateTime, nullable=False)
4171 created = Column('created', DateTime, nullable=False)
4173 data = Column('data', PickleType, nullable=False)
4172 data = Column('data', PickleType, nullable=False)
@@ -1,109 +1,109 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 External module for testing plugins
22 External module for testing plugins
23
23
24 rhodecode.tests.auth_external_test
24 rhodecode.tests.auth_external_test
25
25
26 """
26 """
27 import logging
27 import logging
28 import traceback
28 import traceback
29
29
30 from rhodecode.authentication.base import (
30 from rhodecode.authentication.base import (
31 RhodeCodeExternalAuthPlugin, hybrid_property)
31 RhodeCodeExternalAuthPlugin, hybrid_property)
32 from rhodecode.model.db import User
32 from rhodecode.model.db import User
33 from rhodecode.lib.ext_json import formatted_json
33 from rhodecode.lib.ext_json import formatted_json
34
34
35 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
36
36
37
37
38 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
38 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
39 def __init__(self):
39 def __init__(self):
40 self._logger = logging.getLogger(__name__)
40 self._logger = logging.getLogger(__name__)
41
41
42 @hybrid_property
42 @hybrid_property
43 def allows_creating_users(self):
43 def allows_creating_users(self):
44 return True
44 return True
45
45
46 @hybrid_property
46 @hybrid_property
47 def name(self):
47 def name(self):
48 return "external_test"
48 return "external_test"
49
49
50 def settings(self):
50 def settings(self):
51 settings = [
51 settings = [
52 ]
52 ]
53 return settings
53 return settings
54
54
55 def use_fake_password(self):
55 def use_fake_password(self):
56 return True
56 return True
57
57
58 def user_activation_state(self):
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 return 'hg.extern_activate.auto' in def_user_perms
60 return 'hg.extern_activate.auto' in def_user_perms
61
61
62 def auth(self, userobj, username, password, settings, **kwargs):
62 def auth(self, userobj, username, password, settings, **kwargs):
63 """
63 """
64 Given a user object (which may be null), username, a plaintext password,
64 Given a user object (which may be null), username, a plaintext password,
65 and a settings object (containing all the keys needed as listed in settings()),
65 and a settings object (containing all the keys needed as listed in settings()),
66 authenticate this user's login attempt.
66 authenticate this user's login attempt.
67
67
68 Return None on failure. On success, return a dictionary of the form:
68 Return None on failure. On success, return a dictionary of the form:
69
69
70 see: RhodeCodeAuthPluginBase.auth_func_attrs
70 see: RhodeCodeAuthPluginBase.auth_func_attrs
71 This is later validated for correctness
71 This is later validated for correctness
72 """
72 """
73
73
74 if not username or not password:
74 if not username or not password:
75 log.debug('Empty username or password skipping...')
75 log.debug('Empty username or password skipping...')
76 return None
76 return None
77
77
78 try:
78 try:
79 user_dn = username
79 user_dn = username
80
80
81 # # old attrs fetched from RhodeCode database
81 # # old attrs fetched from RhodeCode database
82 admin = getattr(userobj, 'admin', False)
82 admin = getattr(userobj, 'admin', False)
83 active = getattr(userobj, 'active', True)
83 active = getattr(userobj, 'active', True)
84 email = getattr(userobj, 'email', '')
84 email = getattr(userobj, 'email', '')
85 firstname = getattr(userobj, 'firstname', '')
85 firstname = getattr(userobj, 'firstname', '')
86 lastname = getattr(userobj, 'lastname', '')
86 lastname = getattr(userobj, 'lastname', '')
87 extern_type = getattr(userobj, 'extern_type', '')
87 extern_type = getattr(userobj, 'extern_type', '')
88 #
88 #
89 user_attrs = {
89 user_attrs = {
90 'username': username,
90 'username': username,
91 'firstname': firstname,
91 'firstname': firstname,
92 'lastname': lastname,
92 'lastname': lastname,
93 'groups': [],
93 'groups': [],
94 'email': '%s@rhodecode.com' % username,
94 'email': '%s@rhodecode.com' % username,
95 'admin': admin,
95 'admin': admin,
96 'active': active,
96 'active': active,
97 "active_from_extern": None,
97 "active_from_extern": None,
98 'extern_name': user_dn,
98 'extern_name': user_dn,
99 'extern_type': extern_type,
99 'extern_type': extern_type,
100 }
100 }
101
101
102 log.debug('EXTERNAL user: \n%s' % formatted_json(user_attrs))
102 log.debug('EXTERNAL user: \n%s' % formatted_json(user_attrs))
103 log.info('user %s authenticated correctly' % user_attrs['username'])
103 log.info('user %s authenticated correctly' % user_attrs['username'])
104
104
105 return user_attrs
105 return user_attrs
106
106
107 except (Exception,):
107 except (Exception,):
108 log.error(traceback.format_exc())
108 log.error(traceback.format_exc())
109 return None
109 return None
General Comments 0
You need to be logged in to leave comments. Login now