##// END OF EJS Templates
updated with a latest changes.
ilin.s -
r5536:2c8dbdc5 merge default
parent child Browse files
Show More
@@ -0,0 +1,17 b''
1
2 def apply_license(*args, **kwargs):
3 pass
4
5 try:
6 from rc_license.models import apply_license
7 except ImportError:
8 pass
9
10
11 def apply_license_from_file(*args, **kwargs):
12 pass
13
14 try:
15 from rc_license.models import apply_license_from_file
16 except ImportError:
17 pass
@@ -1,299 +1,299 b''
1 1 # deps, generated via pipdeptree --exclude setuptools,wheel,pipdeptree,pip -f | tr '[:upper:]' '[:lower:]'
2 2
3 3 alembic==1.13.1
4 4 mako==1.2.4
5 5 markupsafe==2.1.2
6 6 sqlalchemy==1.4.52
7 7 greenlet==3.0.3
8 8 typing_extensions==4.12.2
9 9 async-timeout==4.0.3
10 10 babel==2.12.1
11 11 beaker==1.12.1
12 12 celery==5.3.6
13 13 billiard==4.2.0
14 14 click==8.1.3
15 15 click-didyoumean==0.3.0
16 16 click==8.1.3
17 17 click-plugins==1.1.1
18 18 click==8.1.3
19 19 click-repl==0.2.0
20 20 click==8.1.3
21 21 prompt_toolkit==3.0.47
22 22 wcwidth==0.2.13
23 23 six==1.16.0
24 24 kombu==5.3.5
25 25 amqp==5.2.0
26 26 vine==5.1.0
27 27 vine==5.1.0
28 28 python-dateutil==2.8.2
29 29 six==1.16.0
30 30 tzdata==2024.1
31 31 vine==5.1.0
32 32 channelstream==0.7.1
33 33 gevent==24.2.1
34 34 greenlet==3.0.3
35 35 zope.event==5.0.0
36 zope.interface==6.4.post2
36 zope.interface==7.0.3
37 37 itsdangerous==1.1.0
38 38 marshmallow==2.18.0
39 39 pyramid==2.0.2
40 40 hupper==1.12
41 41 plaster==1.1.2
42 42 plaster-pastedeploy==1.0.1
43 43 pastedeploy==3.1.0
44 44 plaster==1.1.2
45 45 translationstring==1.4
46 46 venusian==3.0.0
47 47 webob==1.8.7
48 48 zope.deprecation==5.0.0
49 zope.interface==6.4.post2
49 zope.interface==7.0.3
50 50 pyramid-jinja2==2.10
51 51 jinja2==3.1.2
52 52 markupsafe==2.1.2
53 53 markupsafe==2.1.2
54 54 pyramid==2.0.2
55 55 hupper==1.12
56 56 plaster==1.1.2
57 57 plaster-pastedeploy==1.0.1
58 58 pastedeploy==3.1.0
59 59 plaster==1.1.2
60 60 translationstring==1.4
61 61 venusian==3.0.0
62 62 webob==1.8.7
63 63 zope.deprecation==5.0.0
64 zope.interface==6.4.post2
64 zope.interface==7.0.3
65 65 zope.deprecation==5.0.0
66 66 python-dateutil==2.8.2
67 67 six==1.16.0
68 68 requests==2.28.2
69 69 certifi==2022.12.7
70 70 charset-normalizer==3.1.0
71 71 idna==3.4
72 72 urllib3==1.26.14
73 73 ws4py==0.5.1
74 74 deform==2.0.15
75 75 chameleon==3.10.2
76 76 colander==2.0
77 77 iso8601==1.1.0
78 78 translationstring==1.4
79 79 iso8601==1.1.0
80 80 peppercorn==0.6
81 81 translationstring==1.4
82 82 zope.deprecation==5.0.0
83 83 docutils==0.19
84 84 dogpile.cache==1.3.3
85 85 decorator==5.1.1
86 86 stevedore==5.1.0
87 87 pbr==5.11.1
88 88 formencode==2.1.0
89 89 six==1.16.0
90 fsspec==2024.6.0
90 fsspec==2024.9.0
91 91 gunicorn==23.0.0
92 92 packaging==24.1
93 93 gevent==24.2.1
94 94 greenlet==3.0.3
95 95 zope.event==5.0.0
96 zope.interface==6.4.post2
96 zope.interface==7.0.3
97 97 ipython==8.26.0
98 98 decorator==5.1.1
99 99 jedi==0.19.1
100 100 parso==0.8.4
101 101 matplotlib-inline==0.1.7
102 102 traitlets==5.14.3
103 103 pexpect==4.9.0
104 104 ptyprocess==0.7.0
105 105 prompt_toolkit==3.0.47
106 106 wcwidth==0.2.13
107 107 pygments==2.18.0
108 108 stack-data==0.6.3
109 109 asttokens==2.4.1
110 110 six==1.16.0
111 111 executing==2.0.1
112 112 pure_eval==0.2.3
113 113 traitlets==5.14.3
114 114 typing_extensions==4.12.2
115 115 markdown==3.4.3
116 116 msgpack==1.0.8
117 117 mysqlclient==2.1.1
118 118 nbconvert==7.7.3
119 119 beautifulsoup4==4.12.3
120 120 soupsieve==2.5
121 121 bleach==6.1.0
122 122 six==1.16.0
123 123 webencodings==0.5.1
124 124 defusedxml==0.7.1
125 125 jinja2==3.1.2
126 126 markupsafe==2.1.2
127 127 jupyter_core==5.3.1
128 128 platformdirs==3.10.0
129 129 traitlets==5.14.3
130 130 jupyterlab-pygments==0.2.2
131 131 markupsafe==2.1.2
132 132 mistune==2.0.5
133 133 nbclient==0.8.0
134 134 jupyter_client==8.3.0
135 135 jupyter_core==5.3.1
136 136 platformdirs==3.10.0
137 137 traitlets==5.14.3
138 138 python-dateutil==2.8.2
139 139 six==1.16.0
140 140 pyzmq==25.0.0
141 141 tornado==6.2
142 142 traitlets==5.14.3
143 143 jupyter_core==5.3.1
144 144 platformdirs==3.10.0
145 145 traitlets==5.14.3
146 146 nbformat==5.9.2
147 147 fastjsonschema==2.18.0
148 148 jsonschema==4.18.6
149 149 attrs==22.2.0
150 150 pyrsistent==0.19.3
151 151 jupyter_core==5.3.1
152 152 platformdirs==3.10.0
153 153 traitlets==5.14.3
154 154 traitlets==5.14.3
155 155 traitlets==5.14.3
156 156 nbformat==5.9.2
157 157 fastjsonschema==2.18.0
158 158 jsonschema==4.18.6
159 159 attrs==22.2.0
160 160 pyrsistent==0.19.3
161 161 jupyter_core==5.3.1
162 162 platformdirs==3.10.0
163 163 traitlets==5.14.3
164 164 traitlets==5.14.3
165 165 pandocfilters==1.5.0
166 166 pygments==2.18.0
167 167 tinycss2==1.2.1
168 168 webencodings==0.5.1
169 169 traitlets==5.14.3
170 orjson==3.10.6
170 orjson==3.10.7
171 171 paste==3.10.1
172 172 premailer==3.10.0
173 173 cachetools==5.3.3
174 174 cssselect==1.2.0
175 175 cssutils==2.6.0
176 176 lxml==5.3.0
177 177 requests==2.28.2
178 178 certifi==2022.12.7
179 179 charset-normalizer==3.1.0
180 180 idna==3.4
181 181 urllib3==1.26.14
182 182 psutil==5.9.8
183 183 psycopg2==2.9.9
184 184 py-bcrypt==0.4
185 185 pycmarkgfm==1.2.0
186 186 cffi==1.16.0
187 187 pycparser==2.21
188 188 pycryptodome==3.17
189 189 pycurl==7.45.3
190 190 pymysql==1.0.3
191 191 pyotp==2.8.0
192 192 pyparsing==3.1.1
193 193 pyramid-mailer==0.15.1
194 194 pyramid==2.0.2
195 195 hupper==1.12
196 196 plaster==1.1.2
197 197 plaster-pastedeploy==1.0.1
198 198 pastedeploy==3.1.0
199 199 plaster==1.1.2
200 200 translationstring==1.4
201 201 venusian==3.0.0
202 202 webob==1.8.7
203 203 zope.deprecation==5.0.0
204 zope.interface==6.4.post2
204 zope.interface==7.0.3
205 205 repoze.sendmail==4.4.1
206 transaction==3.1.0
207 zope.interface==6.4.post2
208 zope.interface==6.4.post2
209 transaction==3.1.0
210 zope.interface==6.4.post2
206 transaction==5.0.0
207 zope.interface==7.0.3
208 zope.interface==7.0.3
209 transaction==5.0.0
210 zope.interface==7.0.3
211 211 pyramid-mako==1.1.0
212 212 mako==1.2.4
213 213 markupsafe==2.1.2
214 214 pyramid==2.0.2
215 215 hupper==1.12
216 216 plaster==1.1.2
217 217 plaster-pastedeploy==1.0.1
218 218 pastedeploy==3.1.0
219 219 plaster==1.1.2
220 220 translationstring==1.4
221 221 venusian==3.0.0
222 222 webob==1.8.7
223 223 zope.deprecation==5.0.0
224 zope.interface==6.4.post2
224 zope.interface==7.0.3
225 225 python-ldap==3.4.3
226 226 pyasn1==0.4.8
227 227 pyasn1-modules==0.2.8
228 228 pyasn1==0.4.8
229 229 python-memcached==1.59
230 230 six==1.16.0
231 231 python-pam==2.0.2
232 232 python3-saml==1.16.0
233 233 isodate==0.6.1
234 234 six==1.16.0
235 235 lxml==5.3.0
236 236 xmlsec==1.3.14
237 237 lxml==5.3.0
238 238 pyyaml==6.0.1
239 redis==5.0.4
239 redis==5.1.0
240 240 async-timeout==4.0.3
241 241 regex==2022.10.31
242 242 routes==2.5.1
243 243 repoze.lru==0.7
244 244 six==1.16.0
245 s3fs==2024.6.0
245 s3fs==2024.9.0
246 246 aiobotocore==2.13.0
247 247 aiohttp==3.9.5
248 248 aiosignal==1.3.1
249 249 frozenlist==1.4.1
250 250 attrs==22.2.0
251 251 frozenlist==1.4.1
252 252 multidict==6.0.5
253 253 yarl==1.9.4
254 254 idna==3.4
255 255 multidict==6.0.5
256 256 aioitertools==0.11.0
257 257 botocore==1.34.106
258 258 jmespath==1.0.1
259 259 python-dateutil==2.8.2
260 260 six==1.16.0
261 261 urllib3==1.26.14
262 262 wrapt==1.16.0
263 263 aiohttp==3.9.5
264 264 aiosignal==1.3.1
265 265 frozenlist==1.4.1
266 266 attrs==22.2.0
267 267 frozenlist==1.4.1
268 268 multidict==6.0.5
269 269 yarl==1.9.4
270 270 idna==3.4
271 271 multidict==6.0.5
272 fsspec==2024.6.0
272 fsspec==2024.9.0
273 273 simplejson==3.19.2
274 274 sshpubkeys==3.3.1
275 275 cryptography==40.0.2
276 276 cffi==1.16.0
277 277 pycparser==2.21
278 278 ecdsa==0.18.0
279 279 six==1.16.0
280 280 sqlalchemy==1.4.52
281 281 greenlet==3.0.3
282 282 typing_extensions==4.12.2
283 283 supervisor==4.2.5
284 284 tzlocal==4.3
285 285 pytz-deprecation-shim==0.1.0.post0
286 286 tzdata==2024.1
287 287 tempita==0.5.2
288 288 unidecode==1.3.6
289 289 urlobject==2.4.3
290 290 waitress==3.0.0
291 291 webhelpers2==2.1
292 292 markupsafe==2.1.2
293 293 six==1.16.0
294 294 whoosh==2.7.4
295 295 zope.cachedescriptors==5.0.0
296 296 qrcode==7.4.2
297 297
298 298 ## uncomment to add the debug libraries
299 299 #-r requirements_debug.txt
@@ -1,1124 +1,1124 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 from rhodecode.apps._base import ADMIN_PREFIX
21 21 from rhodecode.apps._base.navigation import includeme as nav_includeme
22 22 from rhodecode.apps.admin.views.main_views import AdminMainView
23 23
24 24
25 25 def admin_routes(config):
26 26 """
27 27 Admin prefixed routes
28 28 """
29 29 from rhodecode.apps.admin.views.audit_logs import AdminAuditLogsView
30 30 from rhodecode.apps.admin.views.artifacts import AdminArtifactsView
31 31 from rhodecode.apps.admin.views.automation import AdminAutomationView
32 32 from rhodecode.apps.admin.views.scheduler import AdminSchedulerView
33 33 from rhodecode.apps.admin.views.defaults import AdminDefaultSettingsView
34 34 from rhodecode.apps.admin.views.exception_tracker import ExceptionsTrackerView
35 35 from rhodecode.apps.admin.views.open_source_licenses import OpenSourceLicensesAdminSettingsView
36 36 from rhodecode.apps.admin.views.permissions import AdminPermissionsView
37 37 from rhodecode.apps.admin.views.process_management import AdminProcessManagementView
38 38 from rhodecode.apps.admin.views.repo_groups import AdminRepoGroupsView
39 39 from rhodecode.apps.admin.views.repositories import AdminReposView
40 40 from rhodecode.apps.admin.views.sessions import AdminSessionSettingsView
41 41 from rhodecode.apps.admin.views.settings import AdminSettingsView
42 42 from rhodecode.apps.admin.views.svn_config import AdminSvnConfigView
43 43 from rhodecode.apps.admin.views.system_info import AdminSystemInfoSettingsView
44 44 from rhodecode.apps.admin.views.user_groups import AdminUserGroupsView
45 45 from rhodecode.apps.admin.views.users import AdminUsersView, UsersView
46 46 from rhodecode.apps.admin.views.security import AdminSecurityView
47 47
48 48 # Security EE feature
49 49
50 50 config.add_route(
51 51 'admin_security',
52 52 pattern='/security')
53 53 config.add_view(
54 54 AdminSecurityView,
55 55 attr='security',
56 56 route_name='admin_security', request_method='GET',
57 57 renderer='rhodecode:templates/admin/security/security.mako')
58 58
59 59 config.add_route(
60 60 name='admin_security_update',
61 61 pattern='/security/update')
62 62 config.add_view(
63 63 AdminSecurityView,
64 64 attr='security_update',
65 65 route_name='admin_security_update', request_method='POST',
66 66 renderer='rhodecode:templates/admin/security/security.mako')
67 67
68 68 config.add_route(
69 69 name='admin_security_modify_allowed_vcs_client_versions',
70 pattern='/security/modify/allowed_vcs_client_versions')
70 pattern=ADMIN_PREFIX + '/security/modify/allowed_vcs_client_versions')
71 71 config.add_view(
72 72 AdminSecurityView,
73 73 attr='vcs_whitelisted_client_versions_edit',
74 74 route_name='admin_security_modify_allowed_vcs_client_versions', request_method=('GET', 'POST'),
75 75 renderer='rhodecode:templates/admin/security/edit_allowed_vcs_client_versions.mako')
76 76
77 77
78 78 config.add_route(
79 79 name='admin_audit_logs',
80 80 pattern='/audit_logs')
81 81 config.add_view(
82 82 AdminAuditLogsView,
83 83 attr='admin_audit_logs',
84 84 route_name='admin_audit_logs', request_method='GET',
85 85 renderer='rhodecode:templates/admin/admin_audit_logs.mako')
86 86
87 87 config.add_route(
88 88 name='admin_audit_log_entry',
89 89 pattern='/audit_logs/{audit_log_id}')
90 90 config.add_view(
91 91 AdminAuditLogsView,
92 92 attr='admin_audit_log_entry',
93 93 route_name='admin_audit_log_entry', request_method='GET',
94 94 renderer='rhodecode:templates/admin/admin_audit_log_entry.mako')
95 95
96 96 # Artifacts EE feature
97 97 config.add_route(
98 98 'admin_artifacts',
99 99 pattern=ADMIN_PREFIX + '/artifacts')
100 100 config.add_route(
101 101 'admin_artifacts_show_all',
102 102 pattern=ADMIN_PREFIX + '/artifacts')
103 103 config.add_view(
104 104 AdminArtifactsView,
105 105 attr='artifacts',
106 106 route_name='admin_artifacts', request_method='GET',
107 107 renderer='rhodecode:templates/admin/artifacts/artifacts.mako')
108 108 config.add_view(
109 109 AdminArtifactsView,
110 110 attr='artifacts',
111 111 route_name='admin_artifacts_show_all', request_method='GET',
112 112 renderer='rhodecode:templates/admin/artifacts/artifacts.mako')
113 113
114 114 # EE views
115 115 config.add_route(
116 116 name='admin_artifacts_show_info',
117 117 pattern=ADMIN_PREFIX + '/artifacts/{uid}')
118 118 config.add_route(
119 119 name='admin_artifacts_delete',
120 120 pattern=ADMIN_PREFIX + '/artifacts/{uid}/delete')
121 121 config.add_route(
122 122 name='admin_artifacts_update',
123 123 pattern=ADMIN_PREFIX + '/artifacts/{uid}/update')
124 124
125 125 # Automation EE feature
126 126 config.add_route(
127 127 'admin_automation',
128 128 pattern=ADMIN_PREFIX + '/automation')
129 129 config.add_view(
130 130 AdminAutomationView,
131 131 attr='automation',
132 132 route_name='admin_automation', request_method='GET',
133 133 renderer='rhodecode:templates/admin/automation/automation.mako')
134 134
135 135 # Scheduler EE feature
136 136 config.add_route(
137 137 'admin_scheduler',
138 138 pattern=ADMIN_PREFIX + '/scheduler')
139 139 config.add_view(
140 140 AdminSchedulerView,
141 141 attr='scheduler',
142 142 route_name='admin_scheduler', request_method='GET',
143 143 renderer='rhodecode:templates/admin/scheduler/scheduler.mako')
144 144
145 145 config.add_route(
146 146 name='admin_settings_open_source',
147 147 pattern='/settings/open_source')
148 148 config.add_view(
149 149 OpenSourceLicensesAdminSettingsView,
150 150 attr='open_source_licenses',
151 151 route_name='admin_settings_open_source', request_method='GET',
152 152 renderer='rhodecode:templates/admin/settings/settings.mako')
153 153
154 154 config.add_route(
155 155 name='admin_settings_vcs_svn_generate_cfg',
156 156 pattern='/settings/vcs/svn_generate_cfg')
157 157 config.add_view(
158 158 AdminSvnConfigView,
159 159 attr='vcs_svn_generate_config',
160 160 route_name='admin_settings_vcs_svn_generate_cfg',
161 161 request_method='POST', renderer='json')
162 162
163 163 config.add_route(
164 164 name='admin_settings_system',
165 165 pattern='/settings/system')
166 166 config.add_view(
167 167 AdminSystemInfoSettingsView,
168 168 attr='settings_system_info',
169 169 route_name='admin_settings_system', request_method='GET',
170 170 renderer='rhodecode:templates/admin/settings/settings.mako')
171 171
172 172 config.add_route(
173 173 name='admin_settings_system_update',
174 174 pattern='/settings/system/updates')
175 175 config.add_view(
176 176 AdminSystemInfoSettingsView,
177 177 attr='settings_system_info_check_update',
178 178 route_name='admin_settings_system_update', request_method='GET',
179 179 renderer='rhodecode:templates/admin/settings/settings_system_update.mako')
180 180
181 181 config.add_route(
182 182 name='admin_settings_exception_tracker',
183 183 pattern='/settings/exceptions')
184 184 config.add_view(
185 185 ExceptionsTrackerView,
186 186 attr='browse_exceptions',
187 187 route_name='admin_settings_exception_tracker', request_method='GET',
188 188 renderer='rhodecode:templates/admin/settings/settings.mako')
189 189
190 190 config.add_route(
191 191 name='admin_settings_exception_tracker_delete_all',
192 192 pattern='/settings/exceptions_delete_all')
193 193 config.add_view(
194 194 ExceptionsTrackerView,
195 195 attr='exception_delete_all',
196 196 route_name='admin_settings_exception_tracker_delete_all', request_method='POST',
197 197 renderer='rhodecode:templates/admin/settings/settings.mako')
198 198
199 199 config.add_route(
200 200 name='admin_settings_exception_tracker_show',
201 201 pattern='/settings/exceptions/{exception_id}')
202 202 config.add_view(
203 203 ExceptionsTrackerView,
204 204 attr='exception_show',
205 205 route_name='admin_settings_exception_tracker_show', request_method='GET',
206 206 renderer='rhodecode:templates/admin/settings/settings.mako')
207 207
208 208 config.add_route(
209 209 name='admin_settings_exception_tracker_delete',
210 210 pattern='/settings/exceptions/{exception_id}/delete')
211 211 config.add_view(
212 212 ExceptionsTrackerView,
213 213 attr='exception_delete',
214 214 route_name='admin_settings_exception_tracker_delete', request_method='POST',
215 215 renderer='rhodecode:templates/admin/settings/settings.mako')
216 216
217 217 config.add_route(
218 218 name='admin_settings_sessions',
219 219 pattern='/settings/sessions')
220 220 config.add_view(
221 221 AdminSessionSettingsView,
222 222 attr='settings_sessions',
223 223 route_name='admin_settings_sessions', request_method='GET',
224 224 renderer='rhodecode:templates/admin/settings/settings.mako')
225 225
226 226 config.add_route(
227 227 name='admin_settings_sessions_cleanup',
228 228 pattern='/settings/sessions/cleanup')
229 229 config.add_view(
230 230 AdminSessionSettingsView,
231 231 attr='settings_sessions_cleanup',
232 232 route_name='admin_settings_sessions_cleanup', request_method='POST')
233 233
234 234 config.add_route(
235 235 name='admin_settings_process_management',
236 236 pattern='/settings/process_management')
237 237 config.add_view(
238 238 AdminProcessManagementView,
239 239 attr='process_management',
240 240 route_name='admin_settings_process_management', request_method='GET',
241 241 renderer='rhodecode:templates/admin/settings/settings.mako')
242 242
243 243 config.add_route(
244 244 name='admin_settings_process_management_data',
245 245 pattern='/settings/process_management/data')
246 246 config.add_view(
247 247 AdminProcessManagementView,
248 248 attr='process_management_data',
249 249 route_name='admin_settings_process_management_data', request_method='GET',
250 250 renderer='rhodecode:templates/admin/settings/settings_process_management_data.mako')
251 251
252 252 config.add_route(
253 253 name='admin_settings_process_management_signal',
254 254 pattern='/settings/process_management/signal')
255 255 config.add_view(
256 256 AdminProcessManagementView,
257 257 attr='process_management_signal',
258 258 route_name='admin_settings_process_management_signal',
259 259 request_method='POST', renderer='json_ext')
260 260
261 261 config.add_route(
262 262 name='admin_settings_process_management_master_signal',
263 263 pattern='/settings/process_management/master_signal')
264 264 config.add_view(
265 265 AdminProcessManagementView,
266 266 attr='process_management_master_signal',
267 267 route_name='admin_settings_process_management_master_signal',
268 268 request_method='POST', renderer='json_ext')
269 269
270 270 # default settings
271 271 config.add_route(
272 272 name='admin_defaults_repositories',
273 273 pattern='/defaults/repositories')
274 274 config.add_view(
275 275 AdminDefaultSettingsView,
276 276 attr='defaults_repository_show',
277 277 route_name='admin_defaults_repositories', request_method='GET',
278 278 renderer='rhodecode:templates/admin/defaults/defaults.mako')
279 279
280 280 config.add_route(
281 281 name='admin_defaults_repositories_update',
282 282 pattern='/defaults/repositories/update')
283 283 config.add_view(
284 284 AdminDefaultSettingsView,
285 285 attr='defaults_repository_update',
286 286 route_name='admin_defaults_repositories_update', request_method='POST',
287 287 renderer='rhodecode:templates/admin/defaults/defaults.mako')
288 288
289 289 # admin settings
290 290
291 291 config.add_route(
292 292 name='admin_settings',
293 293 pattern='/settings')
294 294 config.add_view(
295 295 AdminSettingsView,
296 296 attr='settings_global',
297 297 route_name='admin_settings', request_method='GET',
298 298 renderer='rhodecode:templates/admin/settings/settings.mako')
299 299
300 300 config.add_route(
301 301 name='admin_settings_update',
302 302 pattern='/settings/update')
303 303 config.add_view(
304 304 AdminSettingsView,
305 305 attr='settings_global_update',
306 306 route_name='admin_settings_update', request_method='POST',
307 307 renderer='rhodecode:templates/admin/settings/settings.mako')
308 308
309 309 config.add_route(
310 310 name='admin_settings_global',
311 311 pattern='/settings/global')
312 312 config.add_view(
313 313 AdminSettingsView,
314 314 attr='settings_global',
315 315 route_name='admin_settings_global', request_method='GET',
316 316 renderer='rhodecode:templates/admin/settings/settings.mako')
317 317
318 318 config.add_route(
319 319 name='admin_settings_global_update',
320 320 pattern='/settings/global/update')
321 321 config.add_view(
322 322 AdminSettingsView,
323 323 attr='settings_global_update',
324 324 route_name='admin_settings_global_update', request_method='POST',
325 325 renderer='rhodecode:templates/admin/settings/settings.mako')
326 326
327 327 config.add_route(
328 328 name='admin_settings_vcs',
329 329 pattern='/settings/vcs')
330 330 config.add_view(
331 331 AdminSettingsView,
332 332 attr='settings_vcs',
333 333 route_name='admin_settings_vcs', request_method='GET',
334 334 renderer='rhodecode:templates/admin/settings/settings.mako')
335 335
336 336 config.add_route(
337 337 name='admin_settings_vcs_update',
338 338 pattern='/settings/vcs/update')
339 339 config.add_view(
340 340 AdminSettingsView,
341 341 attr='settings_vcs_update',
342 342 route_name='admin_settings_vcs_update', request_method='POST',
343 343 renderer='rhodecode:templates/admin/settings/settings.mako')
344 344
345 345 config.add_route(
346 346 name='admin_settings_vcs_svn_pattern_delete',
347 347 pattern='/settings/vcs/svn_pattern_delete')
348 348 config.add_view(
349 349 AdminSettingsView,
350 350 attr='settings_vcs_delete_svn_pattern',
351 351 route_name='admin_settings_vcs_svn_pattern_delete', request_method='POST',
352 352 renderer='json_ext', xhr=True)
353 353
354 354 config.add_route(
355 355 name='admin_settings_mapping',
356 356 pattern='/settings/mapping')
357 357 config.add_view(
358 358 AdminSettingsView,
359 359 attr='settings_mapping',
360 360 route_name='admin_settings_mapping', request_method='GET',
361 361 renderer='rhodecode:templates/admin/settings/settings.mako')
362 362
363 363 config.add_route(
364 364 name='admin_settings_mapping_update',
365 365 pattern='/settings/mapping/update')
366 366 config.add_view(
367 367 AdminSettingsView,
368 368 attr='settings_mapping_update',
369 369 route_name='admin_settings_mapping_update', request_method='POST',
370 370 renderer='rhodecode:templates/admin/settings/settings.mako')
371 371
372 372 config.add_route(
373 373 name='admin_settings_visual',
374 374 pattern='/settings/visual')
375 375 config.add_view(
376 376 AdminSettingsView,
377 377 attr='settings_visual',
378 378 route_name='admin_settings_visual', request_method='GET',
379 379 renderer='rhodecode:templates/admin/settings/settings.mako')
380 380
381 381 config.add_route(
382 382 name='admin_settings_visual_update',
383 383 pattern='/settings/visual/update')
384 384 config.add_view(
385 385 AdminSettingsView,
386 386 attr='settings_visual_update',
387 387 route_name='admin_settings_visual_update', request_method='POST',
388 388 renderer='rhodecode:templates/admin/settings/settings.mako')
389 389
390 390 config.add_route(
391 391 name='admin_settings_issuetracker',
392 392 pattern='/settings/issue-tracker')
393 393 config.add_view(
394 394 AdminSettingsView,
395 395 attr='settings_issuetracker',
396 396 route_name='admin_settings_issuetracker', request_method='GET',
397 397 renderer='rhodecode:templates/admin/settings/settings.mako')
398 398
399 399 config.add_route(
400 400 name='admin_settings_issuetracker_update',
401 401 pattern='/settings/issue-tracker/update')
402 402 config.add_view(
403 403 AdminSettingsView,
404 404 attr='settings_issuetracker_update',
405 405 route_name='admin_settings_issuetracker_update', request_method='POST',
406 406 renderer='rhodecode:templates/admin/settings/settings.mako')
407 407
408 408 config.add_route(
409 409 name='admin_settings_issuetracker_test',
410 410 pattern='/settings/issue-tracker/test')
411 411 config.add_view(
412 412 AdminSettingsView,
413 413 attr='settings_issuetracker_test',
414 414 route_name='admin_settings_issuetracker_test', request_method='POST',
415 415 renderer='string', xhr=True)
416 416
417 417 config.add_route(
418 418 name='admin_settings_issuetracker_delete',
419 419 pattern='/settings/issue-tracker/delete')
420 420 config.add_view(
421 421 AdminSettingsView,
422 422 attr='settings_issuetracker_delete',
423 423 route_name='admin_settings_issuetracker_delete', request_method='POST',
424 424 renderer='json_ext', xhr=True)
425 425
426 426 config.add_route(
427 427 name='admin_settings_email',
428 428 pattern='/settings/email')
429 429 config.add_view(
430 430 AdminSettingsView,
431 431 attr='settings_email',
432 432 route_name='admin_settings_email', request_method='GET',
433 433 renderer='rhodecode:templates/admin/settings/settings.mako')
434 434
435 435 config.add_route(
436 436 name='admin_settings_email_update',
437 437 pattern='/settings/email/update')
438 438 config.add_view(
439 439 AdminSettingsView,
440 440 attr='settings_email_update',
441 441 route_name='admin_settings_email_update', request_method='POST',
442 442 renderer='rhodecode:templates/admin/settings/settings.mako')
443 443
444 444 config.add_route(
445 445 name='admin_settings_hooks',
446 446 pattern='/settings/hooks')
447 447 config.add_view(
448 448 AdminSettingsView,
449 449 attr='settings_hooks',
450 450 route_name='admin_settings_hooks', request_method='GET',
451 451 renderer='rhodecode:templates/admin/settings/settings.mako')
452 452
453 453 config.add_route(
454 454 name='admin_settings_hooks_update',
455 455 pattern='/settings/hooks/update')
456 456 config.add_view(
457 457 AdminSettingsView,
458 458 attr='settings_hooks_update',
459 459 route_name='admin_settings_hooks_update', request_method='POST',
460 460 renderer='rhodecode:templates/admin/settings/settings.mako')
461 461
462 462 config.add_route(
463 463 name='admin_settings_hooks_delete',
464 464 pattern='/settings/hooks/delete')
465 465 config.add_view(
466 466 AdminSettingsView,
467 467 attr='settings_hooks_update',
468 468 route_name='admin_settings_hooks_delete', request_method='POST',
469 469 renderer='rhodecode:templates/admin/settings/settings.mako')
470 470
471 471 config.add_route(
472 472 name='admin_settings_search',
473 473 pattern='/settings/search')
474 474 config.add_view(
475 475 AdminSettingsView,
476 476 attr='settings_search',
477 477 route_name='admin_settings_search', request_method='GET',
478 478 renderer='rhodecode:templates/admin/settings/settings.mako')
479 479
480 480 config.add_route(
481 481 name='admin_settings_labs',
482 482 pattern='/settings/labs')
483 483 config.add_view(
484 484 AdminSettingsView,
485 485 attr='settings_labs',
486 486 route_name='admin_settings_labs', request_method='GET',
487 487 renderer='rhodecode:templates/admin/settings/settings.mako')
488 488
489 489 config.add_route(
490 490 name='admin_settings_labs_update',
491 491 pattern='/settings/labs/update')
492 492 config.add_view(
493 493 AdminSettingsView,
494 494 attr='settings_labs_update',
495 495 route_name='admin_settings_labs_update', request_method='POST',
496 496 renderer='rhodecode:templates/admin/settings/settings.mako')
497 497
498 498 # global permissions
499 499
500 500 config.add_route(
501 501 name='admin_permissions_application',
502 502 pattern='/permissions/application')
503 503 config.add_view(
504 504 AdminPermissionsView,
505 505 attr='permissions_application',
506 506 route_name='admin_permissions_application', request_method='GET',
507 507 renderer='rhodecode:templates/admin/permissions/permissions.mako')
508 508
509 509 config.add_route(
510 510 name='admin_permissions_application_update',
511 511 pattern='/permissions/application/update')
512 512 config.add_view(
513 513 AdminPermissionsView,
514 514 attr='permissions_application_update',
515 515 route_name='admin_permissions_application_update', request_method='POST',
516 516 renderer='rhodecode:templates/admin/permissions/permissions.mako')
517 517
518 518 config.add_route(
519 519 name='admin_permissions_global',
520 520 pattern='/permissions/global')
521 521 config.add_view(
522 522 AdminPermissionsView,
523 523 attr='permissions_global',
524 524 route_name='admin_permissions_global', request_method='GET',
525 525 renderer='rhodecode:templates/admin/permissions/permissions.mako')
526 526
527 527 config.add_route(
528 528 name='admin_permissions_global_update',
529 529 pattern='/permissions/global/update')
530 530 config.add_view(
531 531 AdminPermissionsView,
532 532 attr='permissions_global_update',
533 533 route_name='admin_permissions_global_update', request_method='POST',
534 534 renderer='rhodecode:templates/admin/permissions/permissions.mako')
535 535
536 536 config.add_route(
537 537 name='admin_permissions_object',
538 538 pattern='/permissions/object')
539 539 config.add_view(
540 540 AdminPermissionsView,
541 541 attr='permissions_objects',
542 542 route_name='admin_permissions_object', request_method='GET',
543 543 renderer='rhodecode:templates/admin/permissions/permissions.mako')
544 544
545 545 config.add_route(
546 546 name='admin_permissions_object_update',
547 547 pattern='/permissions/object/update')
548 548 config.add_view(
549 549 AdminPermissionsView,
550 550 attr='permissions_objects_update',
551 551 route_name='admin_permissions_object_update', request_method='POST',
552 552 renderer='rhodecode:templates/admin/permissions/permissions.mako')
553 553
554 554 # Branch perms EE feature
555 555 config.add_route(
556 556 name='admin_permissions_branch',
557 557 pattern='/permissions/branch')
558 558 config.add_view(
559 559 AdminPermissionsView,
560 560 attr='permissions_branch',
561 561 route_name='admin_permissions_branch', request_method='GET',
562 562 renderer='rhodecode:templates/admin/permissions/permissions.mako')
563 563
564 564 config.add_route(
565 565 name='admin_permissions_ips',
566 566 pattern='/permissions/ips')
567 567 config.add_view(
568 568 AdminPermissionsView,
569 569 attr='permissions_ips',
570 570 route_name='admin_permissions_ips', request_method='GET',
571 571 renderer='rhodecode:templates/admin/permissions/permissions.mako')
572 572
573 573 config.add_route(
574 574 name='admin_permissions_overview',
575 575 pattern='/permissions/overview')
576 576 config.add_view(
577 577 AdminPermissionsView,
578 578 attr='permissions_overview',
579 579 route_name='admin_permissions_overview', request_method='GET',
580 580 renderer='rhodecode:templates/admin/permissions/permissions.mako')
581 581
582 582 config.add_route(
583 583 name='admin_permissions_auth_token_access',
584 584 pattern='/permissions/auth_token_access')
585 585 config.add_view(
586 586 AdminPermissionsView,
587 587 attr='auth_token_access',
588 588 route_name='admin_permissions_auth_token_access', request_method='GET',
589 589 renderer='rhodecode:templates/admin/permissions/permissions.mako')
590 590
591 591 config.add_route(
592 592 name='admin_permissions_ssh_keys',
593 593 pattern='/permissions/ssh_keys')
594 594 config.add_view(
595 595 AdminPermissionsView,
596 596 attr='ssh_keys',
597 597 route_name='admin_permissions_ssh_keys', request_method='GET',
598 598 renderer='rhodecode:templates/admin/permissions/permissions.mako')
599 599
600 600 config.add_route(
601 601 name='admin_permissions_ssh_keys_data',
602 602 pattern='/permissions/ssh_keys/data')
603 603 config.add_view(
604 604 AdminPermissionsView,
605 605 attr='ssh_keys_data',
606 606 route_name='admin_permissions_ssh_keys_data', request_method='GET',
607 607 renderer='json_ext', xhr=True)
608 608
609 609 config.add_route(
610 610 name='admin_permissions_ssh_keys_update',
611 611 pattern='/permissions/ssh_keys/update')
612 612 config.add_view(
613 613 AdminPermissionsView,
614 614 attr='ssh_keys_update',
615 615 route_name='admin_permissions_ssh_keys_update', request_method='POST',
616 616 renderer='rhodecode:templates/admin/permissions/permissions.mako')
617 617
618 618 # users admin
619 619 config.add_route(
620 620 name='users',
621 621 pattern='/users')
622 622 config.add_view(
623 623 AdminUsersView,
624 624 attr='users_list',
625 625 route_name='users', request_method='GET',
626 626 renderer='rhodecode:templates/admin/users/users.mako')
627 627
628 628 config.add_route(
629 629 name='users_data',
630 630 pattern='/users_data')
631 631 config.add_view(
632 632 AdminUsersView,
633 633 attr='users_list_data',
634 634 # renderer defined below
635 635 route_name='users_data', request_method='GET',
636 636 renderer='json_ext', xhr=True)
637 637
638 638 config.add_route(
639 639 name='users_create',
640 640 pattern='/users/create')
641 641 config.add_view(
642 642 AdminUsersView,
643 643 attr='users_create',
644 644 route_name='users_create', request_method='POST',
645 645 renderer='rhodecode:templates/admin/users/user_add.mako')
646 646
647 647 config.add_route(
648 648 name='users_new',
649 649 pattern='/users/new')
650 650 config.add_view(
651 651 AdminUsersView,
652 652 attr='users_new',
653 653 route_name='users_new', request_method='GET',
654 654 renderer='rhodecode:templates/admin/users/user_add.mako')
655 655
656 656 # user management
657 657 config.add_route(
658 658 name='user_edit',
659 659 pattern=r'/users/{user_id:\d+}/edit',
660 660 user_route=True)
661 661 config.add_view(
662 662 UsersView,
663 663 attr='user_edit',
664 664 route_name='user_edit', request_method='GET',
665 665 renderer='rhodecode:templates/admin/users/user_edit.mako')
666 666
667 667 config.add_route(
668 668 name='user_edit_advanced',
669 669 pattern=r'/users/{user_id:\d+}/edit/advanced',
670 670 user_route=True)
671 671 config.add_view(
672 672 UsersView,
673 673 attr='user_edit_advanced',
674 674 route_name='user_edit_advanced', request_method='GET',
675 675 renderer='rhodecode:templates/admin/users/user_edit.mako')
676 676
677 677 config.add_route(
678 678 name='user_edit_global_perms',
679 679 pattern=r'/users/{user_id:\d+}/edit/global_permissions',
680 680 user_route=True)
681 681 config.add_view(
682 682 UsersView,
683 683 attr='user_edit_global_perms',
684 684 route_name='user_edit_global_perms', request_method='GET',
685 685 renderer='rhodecode:templates/admin/users/user_edit.mako')
686 686
687 687 config.add_route(
688 688 name='user_edit_global_perms_update',
689 689 pattern=r'/users/{user_id:\d+}/edit/global_permissions/update',
690 690 user_route=True)
691 691 config.add_view(
692 692 UsersView,
693 693 attr='user_edit_global_perms_update',
694 694 route_name='user_edit_global_perms_update', request_method='POST',
695 695 renderer='rhodecode:templates/admin/users/user_edit.mako')
696 696
697 697 config.add_route(
698 698 name='user_update',
699 699 pattern=r'/users/{user_id:\d+}/update',
700 700 user_route=True)
701 701 config.add_view(
702 702 UsersView,
703 703 attr='user_update',
704 704 route_name='user_update', request_method='POST',
705 705 renderer='rhodecode:templates/admin/users/user_edit.mako')
706 706
707 707 config.add_route(
708 708 name='user_delete',
709 709 pattern=r'/users/{user_id:\d+}/delete',
710 710 user_route=True)
711 711 config.add_view(
712 712 UsersView,
713 713 attr='user_delete',
714 714 route_name='user_delete', request_method='POST',
715 715 renderer='rhodecode:templates/admin/users/user_edit.mako')
716 716
717 717 config.add_route(
718 718 name='user_enable_force_password_reset',
719 719 pattern=r'/users/{user_id:\d+}/password_reset_enable',
720 720 user_route=True)
721 721 config.add_view(
722 722 UsersView,
723 723 attr='user_enable_force_password_reset',
724 724 route_name='user_enable_force_password_reset', request_method='POST',
725 725 renderer='rhodecode:templates/admin/users/user_edit.mako')
726 726
727 727 config.add_route(
728 728 name='user_disable_force_password_reset',
729 729 pattern=r'/users/{user_id:\d+}/password_reset_disable',
730 730 user_route=True)
731 731 config.add_view(
732 732 UsersView,
733 733 attr='user_disable_force_password_reset',
734 734 route_name='user_disable_force_password_reset', request_method='POST',
735 735 renderer='rhodecode:templates/admin/users/user_edit.mako')
736 736
737 737 config.add_route(
738 738 name='user_create_personal_repo_group',
739 739 pattern=r'/users/{user_id:\d+}/create_repo_group',
740 740 user_route=True)
741 741 config.add_view(
742 742 UsersView,
743 743 attr='user_create_personal_repo_group',
744 744 route_name='user_create_personal_repo_group', request_method='POST',
745 745 renderer='rhodecode:templates/admin/users/user_edit.mako')
746 746
747 747 # user notice
748 748 config.add_route(
749 749 name='user_notice_dismiss',
750 750 pattern=r'/users/{user_id:\d+}/notice_dismiss',
751 751 user_route=True)
752 752 config.add_view(
753 753 UsersView,
754 754 attr='user_notice_dismiss',
755 755 route_name='user_notice_dismiss', request_method='POST',
756 756 renderer='json_ext', xhr=True)
757 757
758 758 # user auth tokens
759 759 config.add_route(
760 760 name='edit_user_auth_tokens',
761 761 pattern=r'/users/{user_id:\d+}/edit/auth_tokens',
762 762 user_route=True)
763 763 config.add_view(
764 764 UsersView,
765 765 attr='auth_tokens',
766 766 route_name='edit_user_auth_tokens', request_method='GET',
767 767 renderer='rhodecode:templates/admin/users/user_edit.mako')
768 768
769 769 config.add_route(
770 770 name='edit_user_auth_tokens_view',
771 771 pattern=r'/users/{user_id:\d+}/edit/auth_tokens/view',
772 772 user_route=True)
773 773 config.add_view(
774 774 UsersView,
775 775 attr='auth_tokens_view',
776 776 route_name='edit_user_auth_tokens_view', request_method='POST',
777 777 renderer='json_ext', xhr=True)
778 778
779 779 config.add_route(
780 780 name='edit_user_auth_tokens_add',
781 781 pattern=r'/users/{user_id:\d+}/edit/auth_tokens/new',
782 782 user_route=True)
783 783 config.add_view(
784 784 UsersView,
785 785 attr='auth_tokens_add',
786 786 route_name='edit_user_auth_tokens_add', request_method='POST')
787 787
788 788 config.add_route(
789 789 name='edit_user_auth_tokens_delete',
790 790 pattern=r'/users/{user_id:\d+}/edit/auth_tokens/delete',
791 791 user_route=True)
792 792 config.add_view(
793 793 UsersView,
794 794 attr='auth_tokens_delete',
795 795 route_name='edit_user_auth_tokens_delete', request_method='POST')
796 796
797 797 # user ssh keys
798 798 config.add_route(
799 799 name='edit_user_ssh_keys',
800 800 pattern=r'/users/{user_id:\d+}/edit/ssh_keys',
801 801 user_route=True)
802 802 config.add_view(
803 803 UsersView,
804 804 attr='ssh_keys',
805 805 route_name='edit_user_ssh_keys', request_method='GET',
806 806 renderer='rhodecode:templates/admin/users/user_edit.mako')
807 807
808 808 config.add_route(
809 809 name='edit_user_ssh_keys_generate_keypair',
810 810 pattern=r'/users/{user_id:\d+}/edit/ssh_keys/generate',
811 811 user_route=True)
812 812 config.add_view(
813 813 UsersView,
814 814 attr='ssh_keys_generate_keypair',
815 815 route_name='edit_user_ssh_keys_generate_keypair', request_method='GET',
816 816 renderer='rhodecode:templates/admin/users/user_edit.mako')
817 817
818 818 config.add_route(
819 819 name='edit_user_ssh_keys_add',
820 820 pattern=r'/users/{user_id:\d+}/edit/ssh_keys/new',
821 821 user_route=True)
822 822 config.add_view(
823 823 UsersView,
824 824 attr='ssh_keys_add',
825 825 route_name='edit_user_ssh_keys_add', request_method='POST')
826 826
827 827 config.add_route(
828 828 name='edit_user_ssh_keys_delete',
829 829 pattern=r'/users/{user_id:\d+}/edit/ssh_keys/delete',
830 830 user_route=True)
831 831 config.add_view(
832 832 UsersView,
833 833 attr='ssh_keys_delete',
834 834 route_name='edit_user_ssh_keys_delete', request_method='POST')
835 835
836 836 # user emails
837 837 config.add_route(
838 838 name='edit_user_emails',
839 839 pattern=r'/users/{user_id:\d+}/edit/emails',
840 840 user_route=True)
841 841 config.add_view(
842 842 UsersView,
843 843 attr='emails',
844 844 route_name='edit_user_emails', request_method='GET',
845 845 renderer='rhodecode:templates/admin/users/user_edit.mako')
846 846
847 847 config.add_route(
848 848 name='edit_user_emails_add',
849 849 pattern=r'/users/{user_id:\d+}/edit/emails/new',
850 850 user_route=True)
851 851 config.add_view(
852 852 UsersView,
853 853 attr='emails_add',
854 854 route_name='edit_user_emails_add', request_method='POST')
855 855
856 856 config.add_route(
857 857 name='edit_user_emails_delete',
858 858 pattern=r'/users/{user_id:\d+}/edit/emails/delete',
859 859 user_route=True)
860 860 config.add_view(
861 861 UsersView,
862 862 attr='emails_delete',
863 863 route_name='edit_user_emails_delete', request_method='POST')
864 864
865 865 # user IPs
866 866 config.add_route(
867 867 name='edit_user_ips',
868 868 pattern=r'/users/{user_id:\d+}/edit/ips',
869 869 user_route=True)
870 870 config.add_view(
871 871 UsersView,
872 872 attr='ips',
873 873 route_name='edit_user_ips', request_method='GET',
874 874 renderer='rhodecode:templates/admin/users/user_edit.mako')
875 875
876 876 config.add_route(
877 877 name='edit_user_ips_add',
878 878 pattern=r'/users/{user_id:\d+}/edit/ips/new',
879 879 user_route_with_default=True) # enabled for default user too
880 880 config.add_view(
881 881 UsersView,
882 882 attr='ips_add',
883 883 route_name='edit_user_ips_add', request_method='POST')
884 884
885 885 config.add_route(
886 886 name='edit_user_ips_delete',
887 887 pattern=r'/users/{user_id:\d+}/edit/ips/delete',
888 888 user_route_with_default=True) # enabled for default user too
889 889 config.add_view(
890 890 UsersView,
891 891 attr='ips_delete',
892 892 route_name='edit_user_ips_delete', request_method='POST')
893 893
894 894 # user perms
895 895 config.add_route(
896 896 name='edit_user_perms_summary',
897 897 pattern=r'/users/{user_id:\d+}/edit/permissions_summary',
898 898 user_route=True)
899 899 config.add_view(
900 900 UsersView,
901 901 attr='user_perms_summary',
902 902 route_name='edit_user_perms_summary', request_method='GET',
903 903 renderer='rhodecode:templates/admin/users/user_edit.mako')
904 904
905 905 config.add_route(
906 906 name='edit_user_perms_summary_json',
907 907 pattern=r'/users/{user_id:\d+}/edit/permissions_summary/json',
908 908 user_route=True)
909 909 config.add_view(
910 910 UsersView,
911 911 attr='user_perms_summary_json',
912 912 route_name='edit_user_perms_summary_json', request_method='GET',
913 913 renderer='json_ext')
914 914
915 915 # user user groups management
916 916 config.add_route(
917 917 name='edit_user_groups_management',
918 918 pattern=r'/users/{user_id:\d+}/edit/groups_management',
919 919 user_route=True)
920 920 config.add_view(
921 921 UsersView,
922 922 attr='groups_management',
923 923 route_name='edit_user_groups_management', request_method='GET',
924 924 renderer='rhodecode:templates/admin/users/user_edit.mako')
925 925
926 926 config.add_route(
927 927 name='edit_user_groups_management_updates',
928 928 pattern=r'/users/{user_id:\d+}/edit/edit_user_groups_management/updates',
929 929 user_route=True)
930 930 config.add_view(
931 931 UsersView,
932 932 attr='groups_management_updates',
933 933 route_name='edit_user_groups_management_updates', request_method='POST')
934 934
935 935 # user audit logs
936 936 config.add_route(
937 937 name='edit_user_audit_logs',
938 938 pattern=r'/users/{user_id:\d+}/edit/audit', user_route=True)
939 939 config.add_view(
940 940 UsersView,
941 941 attr='user_audit_logs',
942 942 route_name='edit_user_audit_logs', request_method='GET',
943 943 renderer='rhodecode:templates/admin/users/user_edit.mako')
944 944
945 945 config.add_route(
946 946 name='edit_user_audit_logs_download',
947 947 pattern=r'/users/{user_id:\d+}/edit/audit/download', user_route=True)
948 948 config.add_view(
949 949 UsersView,
950 950 attr='user_audit_logs_download',
951 951 route_name='edit_user_audit_logs_download', request_method='GET',
952 952 renderer='string')
953 953
954 954 # user caches
955 955 config.add_route(
956 956 name='edit_user_caches',
957 957 pattern=r'/users/{user_id:\d+}/edit/caches',
958 958 user_route=True)
959 959 config.add_view(
960 960 UsersView,
961 961 attr='user_caches',
962 962 route_name='edit_user_caches', request_method='GET',
963 963 renderer='rhodecode:templates/admin/users/user_edit.mako')
964 964
965 965 config.add_route(
966 966 name='edit_user_caches_update',
967 967 pattern=r'/users/{user_id:\d+}/edit/caches/update',
968 968 user_route=True)
969 969 config.add_view(
970 970 UsersView,
971 971 attr='user_caches_update',
972 972 route_name='edit_user_caches_update', request_method='POST')
973 973
974 974 # user-groups admin
975 975 config.add_route(
976 976 name='user_groups',
977 977 pattern='/user_groups')
978 978 config.add_view(
979 979 AdminUserGroupsView,
980 980 attr='user_groups_list',
981 981 route_name='user_groups', request_method='GET',
982 982 renderer='rhodecode:templates/admin/user_groups/user_groups.mako')
983 983
984 984 config.add_route(
985 985 name='user_groups_data',
986 986 pattern='/user_groups_data')
987 987 config.add_view(
988 988 AdminUserGroupsView,
989 989 attr='user_groups_list_data',
990 990 route_name='user_groups_data', request_method='GET',
991 991 renderer='json_ext', xhr=True)
992 992
993 993 config.add_route(
994 994 name='user_groups_new',
995 995 pattern='/user_groups/new')
996 996 config.add_view(
997 997 AdminUserGroupsView,
998 998 attr='user_groups_new',
999 999 route_name='user_groups_new', request_method='GET',
1000 1000 renderer='rhodecode:templates/admin/user_groups/user_group_add.mako')
1001 1001
1002 1002 config.add_route(
1003 1003 name='user_groups_create',
1004 1004 pattern='/user_groups/create')
1005 1005 config.add_view(
1006 1006 AdminUserGroupsView,
1007 1007 attr='user_groups_create',
1008 1008 route_name='user_groups_create', request_method='POST',
1009 1009 renderer='rhodecode:templates/admin/user_groups/user_group_add.mako')
1010 1010
1011 1011 # repos admin
1012 1012 config.add_route(
1013 1013 name='repos',
1014 1014 pattern='/repos')
1015 1015 config.add_view(
1016 1016 AdminReposView,
1017 1017 attr='repository_list',
1018 1018 route_name='repos', request_method='GET',
1019 1019 renderer='rhodecode:templates/admin/repos/repos.mako')
1020 1020
1021 1021 config.add_route(
1022 1022 name='repos_data',
1023 1023 pattern='/repos_data')
1024 1024 config.add_view(
1025 1025 AdminReposView,
1026 1026 attr='repository_list_data',
1027 1027 route_name='repos_data', request_method='GET',
1028 1028 renderer='json_ext', xhr=True)
1029 1029
1030 1030 config.add_route(
1031 1031 name='repo_new',
1032 1032 pattern='/repos/new')
1033 1033 config.add_view(
1034 1034 AdminReposView,
1035 1035 attr='repository_new',
1036 1036 route_name='repo_new', request_method='GET',
1037 1037 renderer='rhodecode:templates/admin/repos/repo_add.mako')
1038 1038
1039 1039 config.add_route(
1040 1040 name='repo_create',
1041 1041 pattern='/repos/create')
1042 1042 config.add_view(
1043 1043 AdminReposView,
1044 1044 attr='repository_create',
1045 1045 route_name='repo_create', request_method='POST',
1046 1046 renderer='rhodecode:templates/admin/repos/repos.mako')
1047 1047
1048 1048 # repo groups admin
1049 1049 config.add_route(
1050 1050 name='repo_groups',
1051 1051 pattern='/repo_groups')
1052 1052 config.add_view(
1053 1053 AdminRepoGroupsView,
1054 1054 attr='repo_group_list',
1055 1055 route_name='repo_groups', request_method='GET',
1056 1056 renderer='rhodecode:templates/admin/repo_groups/repo_groups.mako')
1057 1057
1058 1058 config.add_route(
1059 1059 name='repo_groups_data',
1060 1060 pattern='/repo_groups_data')
1061 1061 config.add_view(
1062 1062 AdminRepoGroupsView,
1063 1063 attr='repo_group_list_data',
1064 1064 route_name='repo_groups_data', request_method='GET',
1065 1065 renderer='json_ext', xhr=True)
1066 1066
1067 1067 config.add_route(
1068 1068 name='repo_group_new',
1069 1069 pattern='/repo_group/new')
1070 1070 config.add_view(
1071 1071 AdminRepoGroupsView,
1072 1072 attr='repo_group_new',
1073 1073 route_name='repo_group_new', request_method='GET',
1074 1074 renderer='rhodecode:templates/admin/repo_groups/repo_group_add.mako')
1075 1075
1076 1076 config.add_route(
1077 1077 name='repo_group_create',
1078 1078 pattern='/repo_group/create')
1079 1079 config.add_view(
1080 1080 AdminRepoGroupsView,
1081 1081 attr='repo_group_create',
1082 1082 route_name='repo_group_create', request_method='POST',
1083 1083 renderer='rhodecode:templates/admin/repo_groups/repo_group_add.mako')
1084 1084
1085 1085
1086 1086 def includeme(config):
1087 1087 # Create admin navigation registry and add it to the pyramid registry.
1088 1088 nav_includeme(config)
1089 1089
1090 1090 # main admin routes
1091 1091 config.add_route(
1092 1092 name='admin_home', pattern=ADMIN_PREFIX)
1093 1093 config.add_view(
1094 1094 AdminMainView,
1095 1095 attr='admin_main',
1096 1096 route_name='admin_home', request_method='GET',
1097 1097 renderer='rhodecode:templates/admin/main.mako')
1098 1098
1099 1099 # pr global redirect
1100 1100 config.add_route(
1101 1101 name='pull_requests_global_0', # backward compat
1102 1102 pattern=ADMIN_PREFIX + r'/pull_requests/{pull_request_id:\d+}')
1103 1103 config.add_view(
1104 1104 AdminMainView,
1105 1105 attr='pull_requests',
1106 1106 route_name='pull_requests_global_0', request_method='GET')
1107 1107
1108 1108 config.add_route(
1109 1109 name='pull_requests_global_1', # backward compat
1110 1110 pattern=ADMIN_PREFIX + r'/pull-requests/{pull_request_id:\d+}')
1111 1111 config.add_view(
1112 1112 AdminMainView,
1113 1113 attr='pull_requests',
1114 1114 route_name='pull_requests_global_1', request_method='GET')
1115 1115
1116 1116 config.add_route(
1117 1117 name='pull_requests_global',
1118 1118 pattern=ADMIN_PREFIX + r'/pull-request/{pull_request_id:\d+}')
1119 1119 config.add_view(
1120 1120 AdminMainView,
1121 1121 attr='pull_requests',
1122 1122 route_name='pull_requests_global', request_method='GET')
1123 1123
1124 1124 config.include(admin_routes, route_prefix=ADMIN_PREFIX)
@@ -1,72 +1,46 b''
1 1 # Copyright (C) 2010-2024 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import logging
20 import formencode
21 20
22 from rhodecode import BACKENDS
23 21 from rhodecode.apps._base import BaseAppView
24 from rhodecode.model.meta import Session
25 from rhodecode.model.settings import SettingsModel
26 from rhodecode.model.forms import WhitelistedVcsClientsForm
27 22 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
28 23
29 24 log = logging.getLogger(__name__)
30 25
31 26
32 27 class AdminSecurityView(BaseAppView):
33 28
34 29 def load_default_context(self):
35 30 c = self._get_local_tmpl_context()
36 31 return c
37 32
38 33 @LoginRequired()
39 34 @HasPermissionAllDecorator('hg.admin')
40 35 def security(self):
41 36 c = self.load_default_context()
42 37 c.active = 'security'
43 38 return self._get_template_context(c)
44 39
40
45 41 @LoginRequired()
46 42 @HasPermissionAllDecorator('hg.admin')
47 def vcs_whitelisted_client_versions_edit(self):
48 _ = self.request.translate
43 def admin_security_modify_allowed_vcs_client_versions(self):
49 44 c = self.load_default_context()
50 render_ctx = {}
51 settings = SettingsModel()
52 form = WhitelistedVcsClientsForm(_, )()
53 if self.request.method == 'POST':
54 try:
55 result = form.to_python(self.request.POST)
56 for k, v in result.items():
57 if v:
58 setting = settings.create_or_update_setting(name=f'{k}_allowed_clients', val=v)
59 Session().add(setting)
60 Session().commit()
61
62 except formencode.Invalid as errors:
63 render_ctx.update({
64 'errors': errors.error_dict
65 })
66 for key in BACKENDS.keys():
67 verbose_name = f"initial_{key}"
68 if existing := settings.get_setting_by_name(name=f'{key}_allowed_clients'):
69 render_ctx[verbose_name] = existing.app_settings_value
70 else:
71 render_ctx[verbose_name] = '*'
72 return self._get_template_context(c, **render_ctx)
45 c.active = 'security'
46 return self._get_template_context(c)
@@ -1,708 +1,707 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 import logging
21 21 import collections
22 22
23 23 import datetime
24 24 import formencode
25 25 import formencode.htmlfill
26 26
27 27 import rhodecode
28 28
29 29 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
30 30 from pyramid.renderers import render
31 31 from pyramid.response import Response
32 32
33 33 from rhodecode.apps._base import BaseAppView
34 34 from rhodecode.apps._base.navigation import navigation_list
35 35 from rhodecode.apps.svn_support import config_keys
36 36 from rhodecode.lib import helpers as h
37 37 from rhodecode.lib.auth import (
38 38 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
39 39 from rhodecode.lib.celerylib import tasks, run_task
40 40 from rhodecode.lib.str_utils import safe_str
41 41 from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_repo_store_path
42 42 from rhodecode.lib.utils2 import str2bool, AttributeDict
43 43 from rhodecode.lib.index import searcher_from_config
44 44
45 45 from rhodecode.model.db import RhodeCodeUi, Repository
46 46 from rhodecode.model.forms import (ApplicationSettingsForm,
47 47 ApplicationUiSettingsForm, ApplicationVisualisationForm,
48 48 LabsSettingsForm, IssueTrackerPatternsForm)
49 49 from rhodecode.model.permission import PermissionModel
50 50 from rhodecode.model.repo_group import RepoGroupModel
51 51
52 52 from rhodecode.model.scm import ScmModel
53 53 from rhodecode.model.notification import EmailNotificationModel
54 54 from rhodecode.model.meta import Session
55 55 from rhodecode.model.settings import (
56 56 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
57 57 SettingsModel)
58 58
59 59
60 60 log = logging.getLogger(__name__)
61 61
62 62
63 63 class AdminSettingsView(BaseAppView):
64 64
65 65 def load_default_context(self):
66 66 c = self._get_local_tmpl_context()
67 67 c.labs_active = str2bool(
68 68 rhodecode.CONFIG.get('labs_settings_active', 'true'))
69 69 c.navlist = navigation_list(self.request)
70 70 return c
71 71
72 72 @classmethod
73 73 def _get_ui_settings(cls):
74 74 ret = RhodeCodeUi.query().all()
75 75
76 76 if not ret:
77 77 raise Exception('Could not get application ui settings !')
78 78 settings = {}
79 79 for each in ret:
80 80 k = each.ui_key
81 81 v = each.ui_value
82 82 if k == '/':
83 83 k = 'root_path'
84 84
85 if k in ['push_ssl', 'publish', 'enabled']:
85 if k in ['publish', 'enabled']:
86 86 v = str2bool(v)
87 87
88 88 if k.find('.') != -1:
89 89 k = k.replace('.', '_')
90 90
91 91 if each.ui_section in ['hooks', 'extensions']:
92 92 v = each.ui_active
93 93
94 94 settings[each.ui_section + '_' + k] = v
95 95 return settings
96 96
97 97 @classmethod
98 98 def _form_defaults(cls):
99 99 defaults = SettingsModel().get_all_settings()
100 100 defaults.update(cls._get_ui_settings())
101 101
102 102 defaults.update({
103 103 'new_svn_branch': '',
104 104 'new_svn_tag': '',
105 105 })
106 106 return defaults
107 107
108 108 @LoginRequired()
109 109 @HasPermissionAllDecorator('hg.admin')
110 110 def settings_vcs(self):
111 111 c = self.load_default_context()
112 112 c.active = 'vcs'
113 113 model = VcsSettingsModel()
114 114 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
115 115 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
116 116 c.svn_generate_config = rhodecode.ConfigGet().get_bool(config_keys.generate_config)
117 117 c.svn_config_path = rhodecode.ConfigGet().get_str(config_keys.config_file_path)
118 118 defaults = self._form_defaults()
119 119
120 120 model.create_largeobjects_dirs_if_needed(defaults['paths_root_path'])
121 121
122 122 data = render('rhodecode:templates/admin/settings/settings.mako',
123 123 self._get_template_context(c), self.request)
124 124 html = formencode.htmlfill.render(
125 125 data,
126 126 defaults=defaults,
127 127 encoding="UTF-8",
128 128 force_defaults=False
129 129 )
130 130 return Response(html)
131 131
132 132 @LoginRequired()
133 133 @HasPermissionAllDecorator('hg.admin')
134 134 @CSRFRequired()
135 135 def settings_vcs_update(self):
136 136 _ = self.request.translate
137 137 c = self.load_default_context()
138 138 c.active = 'vcs'
139 139
140 140 model = VcsSettingsModel()
141 141 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
142 142 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
143 143
144 144 c.svn_generate_config = rhodecode.ConfigGet().get_bool(config_keys.generate_config)
145 145 c.svn_config_path = rhodecode.ConfigGet().get_str(config_keys.config_file_path)
146 146 application_form = ApplicationUiSettingsForm(self.request.translate)()
147 147
148 148 try:
149 149 form_result = application_form.to_python(dict(self.request.POST))
150 150 except formencode.Invalid as errors:
151 151 h.flash(
152 152 _("Some form inputs contain invalid data."),
153 153 category='error')
154 154 data = render('rhodecode:templates/admin/settings/settings.mako',
155 155 self._get_template_context(c), self.request)
156 156 html = formencode.htmlfill.render(
157 157 data,
158 158 defaults=errors.value,
159 159 errors=errors.unpack_errors() or {},
160 160 prefix_error=False,
161 161 encoding="UTF-8",
162 162 force_defaults=False
163 163 )
164 164 return Response(html)
165 165
166 166 try:
167 model.update_global_ssl_setting(form_result['web_push_ssl'])
168 167 model.update_global_hook_settings(form_result)
169 168
170 169 model.create_or_update_global_svn_settings(form_result)
171 170 model.create_or_update_global_hg_settings(form_result)
172 171 model.create_or_update_global_git_settings(form_result)
173 172 model.create_or_update_global_pr_settings(form_result)
174 173 except Exception:
175 174 log.exception("Exception while updating settings")
176 175 h.flash(_('Error occurred during updating '
177 176 'application settings'), category='error')
178 177 else:
179 178 Session().commit()
180 179 h.flash(_('Updated VCS settings'), category='success')
181 180 raise HTTPFound(h.route_path('admin_settings_vcs'))
182 181
183 182 data = render('rhodecode:templates/admin/settings/settings.mako',
184 183 self._get_template_context(c), self.request)
185 184 html = formencode.htmlfill.render(
186 185 data,
187 186 defaults=self._form_defaults(),
188 187 encoding="UTF-8",
189 188 force_defaults=False
190 189 )
191 190 return Response(html)
192 191
193 192 @LoginRequired()
194 193 @HasPermissionAllDecorator('hg.admin')
195 194 @CSRFRequired()
196 195 def settings_vcs_delete_svn_pattern(self):
197 196 delete_pattern_id = self.request.POST.get('delete_svn_pattern')
198 197 model = VcsSettingsModel()
199 198 try:
200 199 model.delete_global_svn_pattern(delete_pattern_id)
201 200 except SettingNotFound:
202 201 log.exception(
203 202 'Failed to delete svn_pattern with id %s', delete_pattern_id)
204 203 raise HTTPNotFound()
205 204
206 205 Session().commit()
207 206 return True
208 207
209 208 @LoginRequired()
210 209 @HasPermissionAllDecorator('hg.admin')
211 210 def settings_mapping(self):
212 211 c = self.load_default_context()
213 212 c.active = 'mapping'
214 213 c.storage_path = get_rhodecode_repo_store_path()
215 214 data = render('rhodecode:templates/admin/settings/settings.mako',
216 215 self._get_template_context(c), self.request)
217 216 html = formencode.htmlfill.render(
218 217 data,
219 218 defaults=self._form_defaults(),
220 219 encoding="UTF-8",
221 220 force_defaults=False
222 221 )
223 222 return Response(html)
224 223
225 224 @LoginRequired()
226 225 @HasPermissionAllDecorator('hg.admin')
227 226 @CSRFRequired()
228 227 def settings_mapping_update(self):
229 228 _ = self.request.translate
230 229 c = self.load_default_context()
231 230 c.active = 'mapping'
232 231 rm_obsolete = self.request.POST.get('destroy', False)
233 232 invalidate_cache = self.request.POST.get('invalidate', False)
234 233 log.debug('rescanning repo location with destroy obsolete=%s', rm_obsolete)
235 234
236 235 if invalidate_cache:
237 236 log.debug('invalidating all repositories cache')
238 237 for repo in Repository.get_all():
239 238 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
240 239
241 240 filesystem_repos = ScmModel().repo_scan()
242 241 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete, force_hooks_rebuild=True)
243 242 PermissionModel().trigger_permission_flush()
244 243
245 244 def _repr(rm_repo):
246 245 return ', '.join(map(safe_str, rm_repo)) or '-'
247 246
248 247 h.flash(_('Repositories successfully '
249 248 'rescanned added: %s ; removed: %s') %
250 249 (_repr(added), _repr(removed)),
251 250 category='success')
252 251 raise HTTPFound(h.route_path('admin_settings_mapping'))
253 252
254 253 @LoginRequired()
255 254 @HasPermissionAllDecorator('hg.admin')
256 255 def settings_global(self):
257 256 c = self.load_default_context()
258 257 c.active = 'global'
259 258 c.personal_repo_group_default_pattern = RepoGroupModel()\
260 259 .get_personal_group_name_pattern()
261 260
262 261 data = render('rhodecode:templates/admin/settings/settings.mako',
263 262 self._get_template_context(c), self.request)
264 263 html = formencode.htmlfill.render(
265 264 data,
266 265 defaults=self._form_defaults(),
267 266 encoding="UTF-8",
268 267 force_defaults=False
269 268 )
270 269 return Response(html)
271 270
272 271 @LoginRequired()
273 272 @HasPermissionAllDecorator('hg.admin')
274 273 @CSRFRequired()
275 274 def settings_global_update(self):
276 275 _ = self.request.translate
277 276 c = self.load_default_context()
278 277 c.active = 'global'
279 278 c.personal_repo_group_default_pattern = RepoGroupModel()\
280 279 .get_personal_group_name_pattern()
281 280 application_form = ApplicationSettingsForm(self.request.translate)()
282 281 try:
283 282 form_result = application_form.to_python(dict(self.request.POST))
284 283 except formencode.Invalid as errors:
285 284 h.flash(
286 285 _("Some form inputs contain invalid data."),
287 286 category='error')
288 287 data = render('rhodecode:templates/admin/settings/settings.mako',
289 288 self._get_template_context(c), self.request)
290 289 html = formencode.htmlfill.render(
291 290 data,
292 291 defaults=errors.value,
293 292 errors=errors.unpack_errors() or {},
294 293 prefix_error=False,
295 294 encoding="UTF-8",
296 295 force_defaults=False
297 296 )
298 297 return Response(html)
299 298
300 299 settings = [
301 300 ('title', 'rhodecode_title', 'unicode'),
302 301 ('realm', 'rhodecode_realm', 'unicode'),
303 302 ('pre_code', 'rhodecode_pre_code', 'unicode'),
304 303 ('post_code', 'rhodecode_post_code', 'unicode'),
305 304 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
306 305 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
307 306 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
308 307 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
309 308 ]
310 309
311 310 try:
312 311 for setting, form_key, type_ in settings:
313 312 sett = SettingsModel().create_or_update_setting(
314 313 setting, form_result[form_key], type_)
315 314 Session().add(sett)
316 315
317 316 Session().commit()
318 317 SettingsModel().invalidate_settings_cache()
319 318 h.flash(_('Updated application settings'), category='success')
320 319 except Exception:
321 320 log.exception("Exception while updating application settings")
322 321 h.flash(
323 322 _('Error occurred during updating application settings'),
324 323 category='error')
325 324
326 325 raise HTTPFound(h.route_path('admin_settings_global'))
327 326
328 327 @LoginRequired()
329 328 @HasPermissionAllDecorator('hg.admin')
330 329 def settings_visual(self):
331 330 c = self.load_default_context()
332 331 c.active = 'visual'
333 332
334 333 data = render('rhodecode:templates/admin/settings/settings.mako',
335 334 self._get_template_context(c), self.request)
336 335 html = formencode.htmlfill.render(
337 336 data,
338 337 defaults=self._form_defaults(),
339 338 encoding="UTF-8",
340 339 force_defaults=False
341 340 )
342 341 return Response(html)
343 342
344 343 @LoginRequired()
345 344 @HasPermissionAllDecorator('hg.admin')
346 345 @CSRFRequired()
347 346 def settings_visual_update(self):
348 347 _ = self.request.translate
349 348 c = self.load_default_context()
350 349 c.active = 'visual'
351 350 application_form = ApplicationVisualisationForm(self.request.translate)()
352 351 try:
353 352 form_result = application_form.to_python(dict(self.request.POST))
354 353 except formencode.Invalid as errors:
355 354 h.flash(
356 355 _("Some form inputs contain invalid data."),
357 356 category='error')
358 357 data = render('rhodecode:templates/admin/settings/settings.mako',
359 358 self._get_template_context(c), self.request)
360 359 html = formencode.htmlfill.render(
361 360 data,
362 361 defaults=errors.value,
363 362 errors=errors.unpack_errors() or {},
364 363 prefix_error=False,
365 364 encoding="UTF-8",
366 365 force_defaults=False
367 366 )
368 367 return Response(html)
369 368
370 369 try:
371 370 settings = [
372 371 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
373 372 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
374 373 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
375 374 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
376 375 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
377 376 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
378 377 ('show_version', 'rhodecode_show_version', 'bool'),
379 378 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
380 379 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
381 380 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
382 381 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
383 382 ('clone_uri_id_tmpl', 'rhodecode_clone_uri_id_tmpl', 'unicode'),
384 383 ('clone_uri_ssh_tmpl', 'rhodecode_clone_uri_ssh_tmpl', 'unicode'),
385 384 ('support_url', 'rhodecode_support_url', 'unicode'),
386 385 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
387 386 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
388 387 ]
389 388 for setting, form_key, type_ in settings:
390 389 sett = SettingsModel().create_or_update_setting(
391 390 setting, form_result[form_key], type_)
392 391 Session().add(sett)
393 392
394 393 Session().commit()
395 394 SettingsModel().invalidate_settings_cache()
396 395 h.flash(_('Updated visualisation settings'), category='success')
397 396 except Exception:
398 397 log.exception("Exception updating visualization settings")
399 398 h.flash(_('Error occurred during updating '
400 399 'visualisation settings'),
401 400 category='error')
402 401
403 402 raise HTTPFound(h.route_path('admin_settings_visual'))
404 403
405 404 @LoginRequired()
406 405 @HasPermissionAllDecorator('hg.admin')
407 406 def settings_issuetracker(self):
408 407 c = self.load_default_context()
409 408 c.active = 'issuetracker'
410 409 defaults = c.rc_config
411 410
412 411 entry_key = 'rhodecode_issuetracker_pat_'
413 412
414 413 c.issuetracker_entries = {}
415 414 for k, v in defaults.items():
416 415 if k.startswith(entry_key):
417 416 uid = k[len(entry_key):]
418 417 c.issuetracker_entries[uid] = None
419 418
420 419 for uid in c.issuetracker_entries:
421 420 c.issuetracker_entries[uid] = AttributeDict({
422 421 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
423 422 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
424 423 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
425 424 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
426 425 })
427 426
428 427 return self._get_template_context(c)
429 428
430 429 @LoginRequired()
431 430 @HasPermissionAllDecorator('hg.admin')
432 431 @CSRFRequired()
433 432 def settings_issuetracker_test(self):
434 433 error_container = []
435 434
436 435 urlified_commit = h.urlify_commit_message(
437 436 self.request.POST.get('test_text', ''),
438 437 'repo_group/test_repo1', error_container=error_container)
439 438 if error_container:
440 439 def converter(inp):
441 440 return h.html_escape(inp)
442 441
443 442 return 'ERRORS: ' + '\n'.join(map(converter, error_container))
444 443
445 444 return urlified_commit
446 445
447 446 @LoginRequired()
448 447 @HasPermissionAllDecorator('hg.admin')
449 448 @CSRFRequired()
450 449 def settings_issuetracker_update(self):
451 450 _ = self.request.translate
452 451 self.load_default_context()
453 452 settings_model = IssueTrackerSettingsModel()
454 453
455 454 try:
456 455 form = IssueTrackerPatternsForm(self.request.translate)()
457 456 data = form.to_python(self.request.POST)
458 457 except formencode.Invalid as errors:
459 458 log.exception('Failed to add new pattern')
460 459 error = errors
461 460 h.flash(_(f'Invalid issue tracker pattern: {error}'),
462 461 category='error')
463 462 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
464 463
465 464 if data:
466 465 for uid in data.get('delete_patterns', []):
467 466 settings_model.delete_entries(uid)
468 467
469 468 for pattern in data.get('patterns', []):
470 469 for setting, value, type_ in pattern:
471 470 sett = settings_model.create_or_update_setting(
472 471 setting, value, type_)
473 472 Session().add(sett)
474 473
475 474 Session().commit()
476 475
477 476 SettingsModel().invalidate_settings_cache()
478 477 h.flash(_('Updated issue tracker entries'), category='success')
479 478 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
480 479
481 480 @LoginRequired()
482 481 @HasPermissionAllDecorator('hg.admin')
483 482 @CSRFRequired()
484 483 def settings_issuetracker_delete(self):
485 484 _ = self.request.translate
486 485 self.load_default_context()
487 486 uid = self.request.POST.get('uid')
488 487 try:
489 488 IssueTrackerSettingsModel().delete_entries(uid)
490 489 except Exception:
491 490 log.exception('Failed to delete issue tracker setting %s', uid)
492 491 raise HTTPNotFound()
493 492
494 493 SettingsModel().invalidate_settings_cache()
495 494 h.flash(_('Removed issue tracker entry.'), category='success')
496 495
497 496 return {'deleted': uid}
498 497
499 498 @LoginRequired()
500 499 @HasPermissionAllDecorator('hg.admin')
501 500 def settings_email(self):
502 501 c = self.load_default_context()
503 502 c.active = 'email'
504 503 c.rhodecode_ini = rhodecode.CONFIG
505 504
506 505 data = render('rhodecode:templates/admin/settings/settings.mako',
507 506 self._get_template_context(c), self.request)
508 507 html = formencode.htmlfill.render(
509 508 data,
510 509 defaults=self._form_defaults(),
511 510 encoding="UTF-8",
512 511 force_defaults=False
513 512 )
514 513 return Response(html)
515 514
516 515 @LoginRequired()
517 516 @HasPermissionAllDecorator('hg.admin')
518 517 @CSRFRequired()
519 518 def settings_email_update(self):
520 519 _ = self.request.translate
521 520 c = self.load_default_context()
522 521 c.active = 'email'
523 522
524 523 test_email = self.request.POST.get('test_email')
525 524
526 525 if not test_email:
527 526 h.flash(_('Please enter email address'), category='error')
528 527 raise HTTPFound(h.route_path('admin_settings_email'))
529 528
530 529 email_kwargs = {
531 530 'date': datetime.datetime.now(),
532 531 'user': self._rhodecode_db_user
533 532 }
534 533
535 534 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
536 535 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
537 536
538 537 recipients = [test_email] if test_email else None
539 538
540 539 run_task(tasks.send_email, recipients, subject,
541 540 email_body_plaintext, email_body)
542 541
543 542 h.flash(_('Send email task created'), category='success')
544 543 raise HTTPFound(h.route_path('admin_settings_email'))
545 544
546 545 @LoginRequired()
547 546 @HasPermissionAllDecorator('hg.admin')
548 547 def settings_hooks(self):
549 548 c = self.load_default_context()
550 549 c.active = 'hooks'
551 550
552 551 model = SettingsModel()
553 552 c.hooks = model.get_builtin_hooks()
554 553 c.custom_hooks = model.get_custom_hooks()
555 554
556 555 data = render('rhodecode:templates/admin/settings/settings.mako',
557 556 self._get_template_context(c), self.request)
558 557 html = formencode.htmlfill.render(
559 558 data,
560 559 defaults=self._form_defaults(),
561 560 encoding="UTF-8",
562 561 force_defaults=False
563 562 )
564 563 return Response(html)
565 564
566 565 @LoginRequired()
567 566 @HasPermissionAllDecorator('hg.admin')
568 567 @CSRFRequired()
569 568 def settings_hooks_update(self):
570 569 _ = self.request.translate
571 570 c = self.load_default_context()
572 571 c.active = 'hooks'
573 572 if c.visual.allow_custom_hooks_settings:
574 573 ui_key = self.request.POST.get('new_hook_ui_key')
575 574 ui_value = self.request.POST.get('new_hook_ui_value')
576 575
577 576 hook_id = self.request.POST.get('hook_id')
578 577 new_hook = False
579 578
580 579 model = SettingsModel()
581 580 try:
582 581 if ui_value and ui_key:
583 582 model.create_or_update_hook(ui_key, ui_value)
584 583 h.flash(_('Added new hook'), category='success')
585 584 new_hook = True
586 585 elif hook_id:
587 586 RhodeCodeUi.delete(hook_id)
588 587 Session().commit()
589 588
590 589 # check for edits
591 590 update = False
592 591 _d = self.request.POST.dict_of_lists()
593 592 for k, v in zip(_d.get('hook_ui_key', []),
594 593 _d.get('hook_ui_value_new', [])):
595 594 model.create_or_update_hook(k, v)
596 595 update = True
597 596
598 597 if update and not new_hook:
599 598 h.flash(_('Updated hooks'), category='success')
600 599 Session().commit()
601 600 except Exception:
602 601 log.exception("Exception during hook creation")
603 602 h.flash(_('Error occurred during hook creation'),
604 603 category='error')
605 604
606 605 raise HTTPFound(h.route_path('admin_settings_hooks'))
607 606
608 607 @LoginRequired()
609 608 @HasPermissionAllDecorator('hg.admin')
610 609 def settings_search(self):
611 610 c = self.load_default_context()
612 611 c.active = 'search'
613 612
614 613 c.searcher = searcher_from_config(self.request.registry.settings)
615 614 c.statistics = c.searcher.statistics(self.request.translate)
616 615
617 616 return self._get_template_context(c)
618 617
619 618 @LoginRequired()
620 619 @HasPermissionAllDecorator('hg.admin')
621 620 def settings_labs(self):
622 621 c = self.load_default_context()
623 622 if not c.labs_active:
624 623 raise HTTPFound(h.route_path('admin_settings'))
625 624
626 625 c.active = 'labs'
627 626 c.lab_settings = _LAB_SETTINGS
628 627
629 628 data = render('rhodecode:templates/admin/settings/settings.mako',
630 629 self._get_template_context(c), self.request)
631 630 html = formencode.htmlfill.render(
632 631 data,
633 632 defaults=self._form_defaults(),
634 633 encoding="UTF-8",
635 634 force_defaults=False
636 635 )
637 636 return Response(html)
638 637
639 638 @LoginRequired()
640 639 @HasPermissionAllDecorator('hg.admin')
641 640 @CSRFRequired()
642 641 def settings_labs_update(self):
643 642 _ = self.request.translate
644 643 c = self.load_default_context()
645 644 c.active = 'labs'
646 645
647 646 application_form = LabsSettingsForm(self.request.translate)()
648 647 try:
649 648 form_result = application_form.to_python(dict(self.request.POST))
650 649 except formencode.Invalid as errors:
651 650 h.flash(
652 651 _("Some form inputs contain invalid data."),
653 652 category='error')
654 653 data = render('rhodecode:templates/admin/settings/settings.mako',
655 654 self._get_template_context(c), self.request)
656 655 html = formencode.htmlfill.render(
657 656 data,
658 657 defaults=errors.value,
659 658 errors=errors.unpack_errors() or {},
660 659 prefix_error=False,
661 660 encoding="UTF-8",
662 661 force_defaults=False
663 662 )
664 663 return Response(html)
665 664
666 665 try:
667 666 session = Session()
668 667 for setting in _LAB_SETTINGS:
669 668 setting_name = setting.key[len('rhodecode_'):]
670 669 sett = SettingsModel().create_or_update_setting(
671 670 setting_name, form_result[setting.key], setting.type)
672 671 session.add(sett)
673 672
674 673 except Exception:
675 674 log.exception('Exception while updating lab settings')
676 675 h.flash(_('Error occurred during updating labs settings'),
677 676 category='error')
678 677 else:
679 678 Session().commit()
680 679 SettingsModel().invalidate_settings_cache()
681 680 h.flash(_('Updated Labs settings'), category='success')
682 681 raise HTTPFound(h.route_path('admin_settings_labs'))
683 682
684 683 data = render('rhodecode:templates/admin/settings/settings.mako',
685 684 self._get_template_context(c), self.request)
686 685 html = formencode.htmlfill.render(
687 686 data,
688 687 defaults=self._form_defaults(),
689 688 encoding="UTF-8",
690 689 force_defaults=False
691 690 )
692 691 return Response(html)
693 692
694 693
695 694 # :param key: name of the setting including the 'rhodecode_' prefix
696 695 # :param type: the RhodeCodeSetting type to use.
697 696 # :param group: the i18ned group in which we should dispaly this setting
698 697 # :param label: the i18ned label we should display for this setting
699 698 # :param help: the i18ned help we should dispaly for this setting
700 699 LabSetting = collections.namedtuple(
701 700 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
702 701
703 702
704 703 # This list has to be kept in sync with the form
705 704 # rhodecode.model.forms.LabsSettingsForm.
706 705 _LAB_SETTINGS = [
707 706
708 707 ]
@@ -1,231 +1,237 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import os
20 20 import tempfile
21 21 import logging
22 22
23 23 from pyramid.settings import asbool
24 24
25 25 from rhodecode.config.settings_maker import SettingsMaker
26 26 from rhodecode.config import utils as config_utils
27 27
28 28 log = logging.getLogger(__name__)
29 29
30 30
31 31 def sanitize_settings_and_apply_defaults(global_config, settings):
32 32 """
33 33 Applies settings defaults and does all type conversion.
34 34
35 35 We would move all settings parsing and preparation into this place, so that
36 36 we have only one place left which deals with this part. The remaining parts
37 37 of the application would start to rely fully on well-prepared settings.
38 38
39 39 This piece would later be split up per topic to avoid a big fat monster
40 40 function.
41 41 """
42 42 jn = os.path.join
43 43
44 44 global_settings_maker = SettingsMaker(global_config)
45 45 global_settings_maker.make_setting('debug', default=False, parser='bool')
46 46 debug_enabled = asbool(global_config.get('debug'))
47 47
48 48 settings_maker = SettingsMaker(settings)
49 49
50 50 settings_maker.make_setting(
51 51 'logging.autoconfigure',
52 52 default=False,
53 53 parser='bool')
54 54
55 logging_conf = jn(os.path.dirname(global_config.get('__file__')), 'logging.ini')
55 ini_loc = os.path.dirname(global_config.get('__file__'))
56 logging_conf = jn(ini_loc, 'logging.ini')
56 57 settings_maker.enable_logging(logging_conf, level='INFO' if debug_enabled else 'DEBUG')
57 58
58 59 # Default includes, possible to change as a user
59 60 pyramid_includes = settings_maker.make_setting('pyramid.includes', [], parser='list:newline')
60 61 log.debug(
61 62 "Using the following pyramid.includes: %s",
62 63 pyramid_includes)
63 64
64 65 settings_maker.make_setting('rhodecode.edition', 'Community Edition')
65 66 settings_maker.make_setting('rhodecode.edition_id', 'CE')
66 67
67 68 if 'mako.default_filters' not in settings:
68 69 # set custom default filters if we don't have it defined
69 70 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
70 71 settings['mako.default_filters'] = 'h_filter'
71 72
72 73 if 'mako.directories' not in settings:
73 74 mako_directories = settings.setdefault('mako.directories', [
74 75 # Base templates of the original application
75 76 'rhodecode:templates',
76 77 ])
77 78 log.debug(
78 79 "Using the following Mako template directories: %s",
79 80 mako_directories)
80 81
81 82 # NOTE(marcink): fix redis requirement for schema of connection since 3.X
82 83 if 'beaker.session.type' in settings and settings['beaker.session.type'] == 'ext:redis':
83 84 raw_url = settings['beaker.session.url']
84 85 if not raw_url.startswith(('redis://', 'rediss://', 'unix://')):
85 86 settings['beaker.session.url'] = 'redis://' + raw_url
86 87
87 88 settings_maker.make_setting('__file__', global_config.get('__file__'))
88 89
89 90 # TODO: johbo: Re-think this, usually the call to config.include
90 91 # should allow to pass in a prefix.
91 92 settings_maker.make_setting('rhodecode.api.url', '/_admin/api')
92 93
93 94 # Sanitize generic settings.
94 95 settings_maker.make_setting('default_encoding', 'UTF-8', parser='list')
95 96 settings_maker.make_setting('gzip_responses', False, parser='bool')
96 97 settings_maker.make_setting('startup.import_repos', 'false', parser='bool')
97 98
99 # License settings.
100 settings_maker.make_setting('license.hide_license_info', False, parser='bool')
101 settings_maker.make_setting('license.import_path', jn(ini_loc, 'rhodecode_enterprise.license'))
102 settings_maker.make_setting('license.import_path_mode', 'if-missing')
103
98 104 # statsd
99 105 settings_maker.make_setting('statsd.enabled', False, parser='bool')
100 106 settings_maker.make_setting('statsd.statsd_host', 'statsd-exporter', parser='string')
101 107 settings_maker.make_setting('statsd.statsd_port', 9125, parser='int')
102 108 settings_maker.make_setting('statsd.statsd_prefix', '')
103 109 settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool')
104 110
105 111 settings_maker.make_setting('vcs.svn.compatible_version', '')
106 112 settings_maker.make_setting('vcs.svn.redis_conn', 'redis://redis:6379/0')
107 113 settings_maker.make_setting('vcs.svn.proxy.enabled', True, parser='bool')
108 114 settings_maker.make_setting('vcs.svn.proxy.host', 'http://svn:8090', parser='string')
109 115 settings_maker.make_setting('vcs.hooks.protocol.v2', 'celery')
110 116 settings_maker.make_setting('vcs.hooks.host', '*')
111 117 settings_maker.make_setting('vcs.scm_app_implementation', 'http')
112 118 settings_maker.make_setting('vcs.server', '')
113 119 settings_maker.make_setting('vcs.server.protocol', 'http')
114 120 settings_maker.make_setting('vcs.server.enable', 'true', parser='bool')
115 121 settings_maker.make_setting('vcs.hooks.direct_calls', 'false', parser='bool')
116 122 settings_maker.make_setting('vcs.start_server', 'false', parser='bool')
117 123 settings_maker.make_setting('vcs.backends', 'hg, git, svn', parser='list')
118 124 settings_maker.make_setting('vcs.connection_timeout', 3600, parser='int')
119 125 settings_maker.make_setting('vcs.git.lfs.storage_location', '/var/opt/rhodecode_repo_store/.cache/git_lfs_store')
120 126 settings_maker.make_setting('vcs.hg.largefiles.storage_location',
121 127 '/var/opt/rhodecode_repo_store/.cache/hg_largefiles_store')
122 128
123 129 settings_maker.make_setting('vcs.methods.cache', True, parser='bool')
124 130
125 131 # repo_store path
126 132 settings_maker.make_setting('repo_store.path', '/var/opt/rhodecode_repo_store')
127 133 # Support legacy values of vcs.scm_app_implementation. Legacy
128 134 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http', or
129 135 # disabled since 4.13 'vcsserver.scm_app' which is now mapped to 'http'.
130 136 scm_app_impl = settings['vcs.scm_app_implementation']
131 137 if scm_app_impl in ['rhodecode.lib.middleware.utils.scm_app_http', 'vcsserver.scm_app']:
132 138 settings['vcs.scm_app_implementation'] = 'http'
133 139
134 140 settings_maker.make_setting('appenlight', False, parser='bool')
135 141
136 142 temp_store = tempfile.gettempdir()
137 143 tmp_cache_dir = jn(temp_store, 'rc_cache')
138 144
139 145 # save default, cache dir, and use it for all backends later.
140 146 default_cache_dir = settings_maker.make_setting(
141 147 'cache_dir',
142 148 default=tmp_cache_dir, default_when_empty=True,
143 149 parser='dir:ensured')
144 150
145 151 # exception store cache
146 152 settings_maker.make_setting(
147 153 'exception_tracker.store_path',
148 154 default=jn(default_cache_dir, 'exc_store'), default_when_empty=True,
149 155 parser='dir:ensured'
150 156 )
151 157
152 158 settings_maker.make_setting(
153 159 'celerybeat-schedule.path',
154 160 default=jn(default_cache_dir, 'celerybeat_schedule', 'celerybeat-schedule.db'), default_when_empty=True,
155 161 parser='file:ensured'
156 162 )
157 163
158 164 # celery
159 165 broker_url = settings_maker.make_setting('celery.broker_url', 'redis://redis:6379/8')
160 166 settings_maker.make_setting('celery.result_backend', broker_url)
161 167
162 168 settings_maker.make_setting('exception_tracker.send_email', False, parser='bool')
163 169 settings_maker.make_setting('exception_tracker.email_prefix', '[RHODECODE ERROR]', default_when_empty=True)
164 170
165 171 # sessions, ensure file since no-value is memory
166 172 settings_maker.make_setting('beaker.session.type', 'file')
167 173 settings_maker.make_setting('beaker.session.data_dir', jn(default_cache_dir, 'session_data'))
168 174
169 175 # cache_general
170 176 settings_maker.make_setting('rc_cache.cache_general.backend', 'dogpile.cache.rc.file_namespace')
171 177 settings_maker.make_setting('rc_cache.cache_general.expiration_time', 60 * 60 * 12, parser='int')
172 178 settings_maker.make_setting('rc_cache.cache_general.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_general.db'))
173 179
174 180 # cache_perms
175 181 settings_maker.make_setting('rc_cache.cache_perms.backend', 'dogpile.cache.rc.file_namespace')
176 182 settings_maker.make_setting('rc_cache.cache_perms.expiration_time', 60 * 60, parser='int')
177 183 settings_maker.make_setting('rc_cache.cache_perms.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_perms_db'))
178 184
179 185 # cache_repo
180 186 settings_maker.make_setting('rc_cache.cache_repo.backend', 'dogpile.cache.rc.file_namespace')
181 187 settings_maker.make_setting('rc_cache.cache_repo.expiration_time', 60 * 60 * 24 * 30, parser='int')
182 188 settings_maker.make_setting('rc_cache.cache_repo.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_repo_db'))
183 189
184 190 # cache_license
185 191 settings_maker.make_setting('rc_cache.cache_license.backend', 'dogpile.cache.rc.file_namespace')
186 192 settings_maker.make_setting('rc_cache.cache_license.expiration_time', 60 * 5, parser='int')
187 193 settings_maker.make_setting('rc_cache.cache_license.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_license_db'))
188 194
189 195 # cache_repo_longterm memory, 96H
190 196 settings_maker.make_setting('rc_cache.cache_repo_longterm.backend', 'dogpile.cache.rc.memory_lru')
191 197 settings_maker.make_setting('rc_cache.cache_repo_longterm.expiration_time', 345600, parser='int')
192 198 settings_maker.make_setting('rc_cache.cache_repo_longterm.max_size', 10000, parser='int')
193 199
194 200 # sql_cache_short
195 201 settings_maker.make_setting('rc_cache.sql_cache_short.backend', 'dogpile.cache.rc.memory_lru')
196 202 settings_maker.make_setting('rc_cache.sql_cache_short.expiration_time', 30, parser='int')
197 203 settings_maker.make_setting('rc_cache.sql_cache_short.max_size', 10000, parser='int')
198 204
199 205 # archive_cache
200 206 settings_maker.make_setting('archive_cache.locking.url', 'redis://redis:6379/1')
201 207 settings_maker.make_setting('archive_cache.backend.type', 'filesystem')
202 208
203 209 settings_maker.make_setting('archive_cache.filesystem.store_dir', jn(default_cache_dir, 'archive_cache'), default_when_empty=True,)
204 210 settings_maker.make_setting('archive_cache.filesystem.cache_shards', 8, parser='int')
205 211 settings_maker.make_setting('archive_cache.filesystem.cache_size_gb', 10, parser='float')
206 212 settings_maker.make_setting('archive_cache.filesystem.eviction_policy', 'least-recently-stored')
207 213
208 214 settings_maker.make_setting('archive_cache.filesystem.retry', False, parser='bool')
209 215 settings_maker.make_setting('archive_cache.filesystem.retry_backoff', 1, parser='int')
210 216 settings_maker.make_setting('archive_cache.filesystem.retry_attempts', 10, parser='int')
211 217
212 218 settings_maker.make_setting('archive_cache.objectstore.url', 'http://s3-minio:9000', default_when_empty=True,)
213 219 settings_maker.make_setting('archive_cache.objectstore.key', '')
214 220 settings_maker.make_setting('archive_cache.objectstore.secret', '')
215 221 settings_maker.make_setting('archive_cache.objectstore.region', 'eu-central-1')
216 222 settings_maker.make_setting('archive_cache.objectstore.bucket', 'rhodecode-archive-cache', default_when_empty=True,)
217 223 settings_maker.make_setting('archive_cache.objectstore.bucket_shards', 8, parser='int')
218 224
219 225 settings_maker.make_setting('archive_cache.objectstore.cache_size_gb', 10, parser='float')
220 226 settings_maker.make_setting('archive_cache.objectstore.eviction_policy', 'least-recently-stored')
221 227
222 228 settings_maker.make_setting('archive_cache.objectstore.retry', False, parser='bool')
223 229 settings_maker.make_setting('archive_cache.objectstore.retry_backoff', 1, parser='int')
224 230 settings_maker.make_setting('archive_cache.objectstore.retry_attempts', 10, parser='int')
225 231
226 232 settings_maker.env_expand()
227 233
228 234 # configure instance id
229 235 config_utils.set_instance_id(settings)
230 236
231 237 return settings
@@ -1,470 +1,471 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import os
20 20 import sys
21 21 import collections
22 22
23 23 import time
24 24 import logging.config
25 25
26 26 from paste.gzipper import make_gzip_middleware
27 27 import pyramid.events
28 28 from pyramid.wsgi import wsgiapp
29 29 from pyramid.config import Configurator
30 30 from pyramid.settings import asbool, aslist
31 31 from pyramid.httpexceptions import (
32 32 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
33 33 from pyramid.renderers import render_to_response
34 34
35 35 from rhodecode.model import meta
36 36 from rhodecode.config import patches
37 37
38 38 from rhodecode.config.environment import load_pyramid_environment, propagate_rhodecode_config
39 39
40 40 import rhodecode.events
41 41 from rhodecode.config.config_maker import sanitize_settings_and_apply_defaults
42 42 from rhodecode.lib.middleware.vcs import VCSMiddleware
43 43 from rhodecode.lib.request import Request
44 44 from rhodecode.lib.vcs import VCSCommunicationError
45 45 from rhodecode.lib.exceptions import VCSServerUnavailable
46 46 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
47 47 from rhodecode.lib.middleware.https_fixup import HttpsFixup
48 48 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
49 49 from rhodecode.lib.utils2 import AttributeDict
50 50 from rhodecode.lib.exc_tracking import store_exception, format_exc
51 51 from rhodecode.subscribers import (
52 52 scan_repositories_if_enabled, write_js_routes_if_enabled,
53 write_metadata_if_needed, write_usage_data)
53 write_metadata_if_needed, write_usage_data, import_license_if_present)
54 54 from rhodecode.lib.statsd_client import StatsdClient
55 55
56 56 log = logging.getLogger(__name__)
57 57
58 58
59 59 def is_http_error(response):
60 60 # error which should have traceback
61 61 return response.status_code > 499
62 62
63 63
64 64 def should_load_all():
65 65 """
66 66 Returns if all application components should be loaded. In some cases it's
67 67 desired to skip apps loading for faster shell script execution
68 68 """
69 69 ssh_cmd = os.environ.get('RC_CMD_SSH_WRAPPER')
70 70 if ssh_cmd:
71 71 return False
72 72
73 73 return True
74 74
75 75
76 76 def make_pyramid_app(global_config, **settings):
77 77 """
78 78 Constructs the WSGI application based on Pyramid.
79 79
80 80 Specials:
81 81
82 82 * The application can also be integrated like a plugin via the call to
83 83 `includeme`. This is accompanied with the other utility functions which
84 84 are called. Changing this should be done with great care to not break
85 85 cases when these fragments are assembled from another place.
86 86
87 87 """
88 88 start_time = time.time()
89 89 log.info('Pyramid app config starting')
90 90
91 91 sanitize_settings_and_apply_defaults(global_config, settings)
92 92
93 93 # init and bootstrap StatsdClient
94 94 StatsdClient.setup(settings)
95 95
96 96 config = Configurator(settings=settings)
97 97 # Init our statsd at very start
98 98 config.registry.statsd = StatsdClient.statsd
99 99
100 100 # Apply compatibility patches
101 101 patches.inspect_getargspec()
102 102 patches.repoze_sendmail_lf_fix()
103 103
104 104 load_pyramid_environment(global_config, settings)
105 105
106 106 # Static file view comes first
107 107 includeme_first(config)
108 108
109 109 includeme(config)
110 110
111 111 pyramid_app = config.make_wsgi_app()
112 112 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
113 113 pyramid_app.config = config
114 114
115 115 celery_settings = get_celery_config(settings)
116 116 config.configure_celery(celery_settings)
117 117
118 118 # final config set...
119 119 propagate_rhodecode_config(global_config, settings, config.registry.settings)
120 120
121 121 # creating the app uses a connection - return it after we are done
122 122 meta.Session.remove()
123 123
124 124 total_time = time.time() - start_time
125 125 log.info('Pyramid app created and configured in %.2fs', total_time)
126 126 return pyramid_app
127 127
128 128
129 129 def get_celery_config(settings):
130 130 """
131 131 Converts basic ini configuration into celery 4.X options
132 132 """
133 133
134 134 def key_converter(key_name):
135 135 pref = 'celery.'
136 136 if key_name.startswith(pref):
137 137 return key_name[len(pref):].replace('.', '_').lower()
138 138
139 139 def type_converter(parsed_key, value):
140 140 # cast to int
141 141 if value.isdigit():
142 142 return int(value)
143 143
144 144 # cast to bool
145 145 if value.lower() in ['true', 'false', 'True', 'False']:
146 146 return value.lower() == 'true'
147 147 return value
148 148
149 149 celery_config = {}
150 150 for k, v in settings.items():
151 151 pref = 'celery.'
152 152 if k.startswith(pref):
153 153 celery_config[key_converter(k)] = type_converter(key_converter(k), v)
154 154
155 155 # TODO:rethink if we want to support celerybeat based file config, probably NOT
156 156 # beat_config = {}
157 157 # for section in parser.sections():
158 158 # if section.startswith('celerybeat:'):
159 159 # name = section.split(':', 1)[1]
160 160 # beat_config[name] = get_beat_config(parser, section)
161 161
162 162 # final compose of settings
163 163 celery_settings = {}
164 164
165 165 if celery_config:
166 166 celery_settings.update(celery_config)
167 167 # if beat_config:
168 168 # celery_settings.update({'beat_schedule': beat_config})
169 169
170 170 return celery_settings
171 171
172 172
173 173 def not_found_view(request):
174 174 """
175 175 This creates the view which should be registered as not-found-view to
176 176 pyramid.
177 177 """
178 178
179 179 if not getattr(request, 'vcs_call', None):
180 180 # handle like regular case with our error_handler
181 181 return error_handler(HTTPNotFound(), request)
182 182
183 183 # handle not found view as a vcs call
184 184 settings = request.registry.settings
185 185 ae_client = getattr(request, 'ae_client', None)
186 186 vcs_app = VCSMiddleware(
187 187 HTTPNotFound(), request.registry, settings,
188 188 appenlight_client=ae_client)
189 189
190 190 return wsgiapp(vcs_app)(None, request)
191 191
192 192
193 193 def error_handler(exception, request):
194 194 import rhodecode
195 195 from rhodecode.lib import helpers
196 196
197 197 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
198 198
199 199 base_response = HTTPInternalServerError()
200 200 # prefer original exception for the response since it may have headers set
201 201 if isinstance(exception, HTTPException):
202 202 base_response = exception
203 203 elif isinstance(exception, VCSCommunicationError):
204 204 base_response = VCSServerUnavailable()
205 205
206 206 if is_http_error(base_response):
207 207 traceback_info = format_exc(request.exc_info)
208 208 log.error(
209 209 'error occurred handling this request for path: %s, \n%s',
210 210 request.path, traceback_info)
211 211
212 212 error_explanation = base_response.explanation or str(base_response)
213 213 if base_response.status_code == 404:
214 214 error_explanation += " Optionally you don't have permission to access this page."
215 215 c = AttributeDict()
216 216 c.error_message = base_response.status
217 217 c.error_explanation = error_explanation
218 218 c.visual = AttributeDict()
219 219
220 220 c.visual.rhodecode_support_url = (
221 221 request.registry.settings.get('rhodecode_support_url') or
222 222 request.route_url('rhodecode_support')
223 223 )
224 224 c.redirect_time = 0
225 225 c.rhodecode_name = rhodecode_title
226 226 if not c.rhodecode_name:
227 227 c.rhodecode_name = 'Rhodecode'
228 228
229 229 c.causes = []
230 230 if is_http_error(base_response):
231 231 c.causes.append('Server is overloaded.')
232 232 c.causes.append('Server database connection is lost.')
233 233 c.causes.append('Server expected unhandled error.')
234 234
235 235 if hasattr(base_response, 'causes'):
236 236 c.causes = base_response.causes
237 237
238 238 c.messages = helpers.flash.pop_messages(request=request)
239 239 exc_info = sys.exc_info()
240 240 c.exception_id = id(exc_info)
241 241 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
242 242 or base_response.status_code > 499
243 243 c.exception_id_url = request.route_url(
244 244 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
245 245
246 246 debug_mode = rhodecode.ConfigGet().get_bool('debug')
247 247 if c.show_exception_id:
248 248 store_exception(c.exception_id, exc_info)
249 249 c.exception_debug = debug_mode
250 250 c.exception_config_ini = rhodecode.CONFIG.get('__file__')
251 251
252 252 if debug_mode:
253 253 try:
254 254 from rich.traceback import install
255 255 install(show_locals=True)
256 256 log.debug('Installing rich tracebacks...')
257 257 except ImportError:
258 258 pass
259 259
260 260 response = render_to_response(
261 261 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
262 262 response=base_response)
263 263
264 264 response.headers["X-RC-Exception-Id"] = str(c.exception_id)
265 265
266 266 statsd = request.registry.statsd
267 267 if statsd and base_response.status_code > 499:
268 268 exc_type = f"{exception.__class__.__module__}.{exception.__class__.__name__}"
269 269 statsd.incr('rhodecode_exception_total',
270 270 tags=["exc_source:web",
271 271 f"http_code:{base_response.status_code}",
272 272 f"type:{exc_type}"])
273 273
274 274 return response
275 275
276 276
277 277 def includeme_first(config):
278 278 # redirect automatic browser favicon.ico requests to correct place
279 279 def favicon_redirect(context, request):
280 280 return HTTPFound(
281 281 request.static_path('rhodecode:public/images/favicon.ico'))
282 282
283 283 config.add_view(favicon_redirect, route_name='favicon')
284 284 config.add_route('favicon', '/favicon.ico')
285 285
286 286 def robots_redirect(context, request):
287 287 return HTTPFound(
288 288 request.static_path('rhodecode:public/robots.txt'))
289 289
290 290 config.add_view(robots_redirect, route_name='robots')
291 291 config.add_route('robots', '/robots.txt')
292 292
293 293 config.add_static_view(
294 294 '_static/deform', 'deform:static')
295 295 config.add_static_view(
296 296 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
297 297
298 298
299 299 ce_auth_resources = [
300 300 'rhodecode.authentication.plugins.auth_crowd',
301 301 'rhodecode.authentication.plugins.auth_headers',
302 302 'rhodecode.authentication.plugins.auth_jasig_cas',
303 303 'rhodecode.authentication.plugins.auth_ldap',
304 304 'rhodecode.authentication.plugins.auth_pam',
305 305 'rhodecode.authentication.plugins.auth_rhodecode',
306 306 'rhodecode.authentication.plugins.auth_token',
307 307 ]
308 308
309 309
310 310 def includeme(config, auth_resources=None):
311 311 from rhodecode.lib.celerylib.loader import configure_celery
312 312 log.debug('Initializing main includeme from %s', os.path.basename(__file__))
313 313 settings = config.registry.settings
314 314 config.set_request_factory(Request)
315 315
316 316 # plugin information
317 317 config.registry.rhodecode_plugins = collections.OrderedDict()
318 318
319 319 config.add_directive(
320 320 'register_rhodecode_plugin', register_rhodecode_plugin)
321 321
322 322 config.add_directive('configure_celery', configure_celery)
323 323
324 324 if settings.get('appenlight', False):
325 325 config.include('appenlight_client.ext.pyramid_tween')
326 326
327 327 load_all = should_load_all()
328 328
329 329 # Includes which are required. The application would fail without them.
330 330 config.include('pyramid_mako')
331 331 config.include('rhodecode.lib.rc_beaker')
332 332 config.include('rhodecode.lib.rc_cache')
333 333 config.include('rhodecode.lib.archive_cache')
334 334
335 335 config.include('rhodecode.apps._base.navigation')
336 336 config.include('rhodecode.apps._base.subscribers')
337 337 config.include('rhodecode.tweens')
338 338 config.include('rhodecode.authentication')
339 339
340 340 if load_all:
341 341
342 342 # load CE authentication plugins
343 343
344 344 if auth_resources:
345 345 ce_auth_resources.extend(auth_resources)
346 346
347 347 for resource in ce_auth_resources:
348 348 config.include(resource)
349 349
350 350 # Auto discover authentication plugins and include their configuration.
351 351 if asbool(settings.get('auth_plugin.import_legacy_plugins', 'true')):
352 352 from rhodecode.authentication import discover_legacy_plugins
353 353 discover_legacy_plugins(config)
354 354
355 355 # apps
356 356 if load_all:
357 357 log.debug('Starting config.include() calls')
358 358 config.include('rhodecode.api.includeme')
359 359 config.include('rhodecode.apps._base.includeme')
360 360 config.include('rhodecode.apps._base.navigation.includeme')
361 361 config.include('rhodecode.apps._base.subscribers.includeme')
362 362 config.include('rhodecode.apps.hovercards.includeme')
363 363 config.include('rhodecode.apps.ops.includeme')
364 364 config.include('rhodecode.apps.channelstream.includeme')
365 365 config.include('rhodecode.apps.file_store.includeme')
366 366 config.include('rhodecode.apps.admin.includeme')
367 367 config.include('rhodecode.apps.login.includeme')
368 368 config.include('rhodecode.apps.home.includeme')
369 369 config.include('rhodecode.apps.journal.includeme')
370 370
371 371 config.include('rhodecode.apps.repository.includeme')
372 372 config.include('rhodecode.apps.repo_group.includeme')
373 373 config.include('rhodecode.apps.user_group.includeme')
374 374 config.include('rhodecode.apps.search.includeme')
375 375 config.include('rhodecode.apps.user_profile.includeme')
376 376 config.include('rhodecode.apps.user_group_profile.includeme')
377 377 config.include('rhodecode.apps.my_account.includeme')
378 378 config.include('rhodecode.apps.gist.includeme')
379 379
380 380 config.include('rhodecode.apps.svn_support.includeme')
381 381 config.include('rhodecode.apps.ssh_support.includeme')
382 382 config.include('rhodecode.apps.debug_style')
383 383
384 384 if load_all:
385 385 config.include('rhodecode.integrations.includeme')
386 386 config.include('rhodecode.integrations.routes.includeme')
387 387
388 388 config.add_route('rhodecode_support', 'https://rhodecode.com/help/', static=True)
389 389 settings['default_locale_name'] = settings.get('lang', 'en')
390 390 config.add_translation_dirs('rhodecode:i18n/')
391 391
392 392 # Add subscribers.
393 393 if load_all:
394 394 log.debug('Adding subscribers...')
395 395 config.add_subscriber(scan_repositories_if_enabled,
396 396 pyramid.events.ApplicationCreated)
397 397 config.add_subscriber(write_metadata_if_needed,
398 398 pyramid.events.ApplicationCreated)
399 399 config.add_subscriber(write_usage_data,
400 400 pyramid.events.ApplicationCreated)
401 401 config.add_subscriber(write_js_routes_if_enabled,
402 402 pyramid.events.ApplicationCreated)
403
403 config.add_subscriber(import_license_if_present,
404 pyramid.events.ApplicationCreated)
404 405
405 406 # Set the default renderer for HTML templates to mako.
406 407 config.add_mako_renderer('.html')
407 408
408 409 config.add_renderer(
409 410 name='json_ext',
410 411 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
411 412
412 413 config.add_renderer(
413 414 name='string_html',
414 415 factory='rhodecode.lib.string_renderer.html')
415 416
416 417 # include RhodeCode plugins
417 418 includes = aslist(settings.get('rhodecode.includes', []))
418 419 log.debug('processing rhodecode.includes data...')
419 420 for inc in includes:
420 421 config.include(inc)
421 422
422 423 # custom not found view, if our pyramid app doesn't know how to handle
423 424 # the request pass it to potential VCS handling ap
424 425 config.add_notfound_view(not_found_view)
425 426 if not settings.get('debugtoolbar.enabled', False):
426 427 # disabled debugtoolbar handle all exceptions via the error_handlers
427 428 config.add_view(error_handler, context=Exception)
428 429
429 430 # all errors including 403/404/50X
430 431 config.add_view(error_handler, context=HTTPError)
431 432
432 433
433 434 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
434 435 """
435 436 Apply outer WSGI middlewares around the application.
436 437 """
437 438 registry = config.registry
438 439 settings = registry.settings
439 440
440 441 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
441 442 pyramid_app = HttpsFixup(pyramid_app, settings)
442 443
443 444 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
444 445 pyramid_app, settings)
445 446 registry.ae_client = _ae_client
446 447
447 448 if settings['gzip_responses']:
448 449 pyramid_app = make_gzip_middleware(
449 450 pyramid_app, settings, compress_level=1)
450 451
451 452 # this should be the outer most middleware in the wsgi stack since
452 453 # middleware like Routes make database calls
453 454 def pyramid_app_with_cleanup(environ, start_response):
454 455 start = time.time()
455 456 try:
456 457 return pyramid_app(environ, start_response)
457 458 finally:
458 459 # Dispose current database session and rollback uncommitted
459 460 # transactions.
460 461 meta.Session.remove()
461 462
462 463 # In a single threaded mode server, on non sqlite db we should have
463 464 # '0 Current Checked out connections' at the end of a request,
464 465 # if not, then something, somewhere is leaving a connection open
465 466 pool = meta.get_engine().pool
466 467 log.debug('sa pool status: %s', pool.status())
467 468 total = time.time() - start
468 469 log.debug('Request processing finalized: %.4fs', total)
469 470
470 471 return pyramid_app_with_cleanup
@@ -1,679 +1,678 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 Database creation, and setup module for RhodeCode Enterprise. Used for creation
21 21 of database as well as for migration operations
22 22 """
23 23
24 24 import os
25 25 import sys
26 26 import time
27 27 import uuid
28 28 import logging
29 29 import getpass
30 30 from os.path import dirname as dn, join as jn
31 31
32 32 from sqlalchemy.engine import create_engine
33 33
34 34 from rhodecode import __dbversion__
35 35 from rhodecode.model import init_model
36 36 from rhodecode.model.user import UserModel
37 37 from rhodecode.model.db import (
38 38 User, Permission, RhodeCodeUi, RhodeCodeSetting, UserToPerm,
39 39 DbMigrateVersion, RepoGroup, UserRepoGroupToPerm, CacheKey, Repository)
40 40 from rhodecode.model.meta import Session, Base
41 41 from rhodecode.model.permission import PermissionModel
42 42 from rhodecode.model.repo import RepoModel
43 43 from rhodecode.model.repo_group import RepoGroupModel
44 44 from rhodecode.model.settings import SettingsModel
45 45
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 def notify(msg):
51 51 """
52 52 Notification for migrations messages
53 53 """
54 54 ml = len(msg) + (4 * 2)
55 55 print((('\n%s\n*** %s ***\n%s' % ('*' * ml, msg, '*' * ml)).upper()))
56 56
57 57
58 58 class DbManage(object):
59 59
60 60 def __init__(self, log_sql, dbconf, root, tests=False,
61 61 SESSION=None, cli_args=None, enc_key=b''):
62 62
63 63 self.dbname = dbconf.split('/')[-1]
64 64 self.tests = tests
65 65 self.root = root
66 66 self.dburi = dbconf
67 67 self.log_sql = log_sql
68 68 self.cli_args = cli_args or {}
69 69 self.sa = None
70 70 self.engine = None
71 71 self.enc_key = enc_key
72 72 # sets .sa .engine
73 73 self.init_db(SESSION=SESSION)
74 74
75 75 self.ask_ok = self.get_ask_ok_func(self.cli_args.get('force_ask'))
76 76
77 77 def db_exists(self):
78 78 if not self.sa:
79 79 self.init_db()
80 80 try:
81 81 self.sa.query(RhodeCodeUi)\
82 82 .filter(RhodeCodeUi.ui_key == '/')\
83 83 .scalar()
84 84 return True
85 85 except Exception:
86 86 return False
87 87 finally:
88 88 self.sa.rollback()
89 89
90 90 def get_ask_ok_func(self, param):
91 91 if param not in [None]:
92 92 # return a function lambda that has a default set to param
93 93 return lambda *args, **kwargs: param
94 94 else:
95 95 from rhodecode.lib.utils import ask_ok
96 96 return ask_ok
97 97
98 98 def init_db(self, SESSION=None):
99 99
100 100 if SESSION:
101 101 self.sa = SESSION
102 102 self.engine = SESSION.bind
103 103 else:
104 104 # init new sessions
105 105 engine = create_engine(self.dburi, echo=self.log_sql)
106 106 init_model(engine, encryption_key=self.enc_key)
107 107 self.sa = Session()
108 108 self.engine = engine
109 109
110 110 def create_tables(self, override=False):
111 111 """
112 112 Create a auth database
113 113 """
114 114
115 115 log.info("Existing database with the same name is going to be destroyed.")
116 116 log.info("Setup command will run DROP ALL command on that database.")
117 117 engine = self.engine
118 118
119 119 if self.tests:
120 120 destroy = True
121 121 else:
122 122 destroy = self.ask_ok('Are you sure that you want to destroy the old database? [y/n]')
123 123 if not destroy:
124 124 log.info('db tables bootstrap: Nothing done.')
125 125 sys.exit(0)
126 126 if destroy:
127 127 Base.metadata.drop_all(bind=engine)
128 128
129 129 checkfirst = not override
130 130 Base.metadata.create_all(bind=engine, checkfirst=checkfirst)
131 131 log.info('Created tables for %s', self.dbname)
132 132
133 133 def set_db_version(self):
134 134 ver = DbMigrateVersion()
135 135 ver.version = __dbversion__
136 136 ver.repository_id = 'rhodecode_db_migrations'
137 137 ver.repository_path = 'versions'
138 138 self.sa.add(ver)
139 139 log.info('db version set to: %s', __dbversion__)
140 140
141 141 def run_post_migration_tasks(self):
142 142 """
143 143 Run various tasks before actually doing migrations
144 144 """
145 145 # delete cache keys on each upgrade
146 146 total = CacheKey.query().count()
147 147 log.info("Deleting (%s) cache keys now...", total)
148 148 CacheKey.delete_all_cache()
149 149
150 150 def upgrade(self, version=None):
151 151 """
152 152 Upgrades given database schema to given revision following
153 153 all needed steps, to perform the upgrade
154 154
155 155 """
156 156
157 157 from rhodecode.lib.dbmigrate.migrate.versioning import api
158 158 from rhodecode.lib.dbmigrate.migrate.exceptions import DatabaseNotControlledError
159 159
160 160 if 'sqlite' in self.dburi:
161 161 print(
162 162 '********************** WARNING **********************\n'
163 163 'Make sure your version of sqlite is at least 3.7.X. \n'
164 164 'Earlier versions are known to fail on some migrations\n'
165 165 '*****************************************************\n')
166 166
167 167 upgrade = self.ask_ok(
168 168 'You are about to perform a database upgrade. Make '
169 169 'sure you have backed up your database. '
170 170 'Continue ? [y/n]')
171 171 if not upgrade:
172 172 log.info('No upgrade performed')
173 173 sys.exit(0)
174 174
175 175 repository_path = jn(dn(dn(dn(os.path.realpath(__file__)))),
176 176 'rhodecode/lib/dbmigrate')
177 177 db_uri = self.dburi
178 178
179 179 if version:
180 180 DbMigrateVersion.set_version(version)
181 181
182 182 try:
183 183 curr_version = api.db_version(db_uri, repository_path)
184 184 msg = (f'Found current database db_uri under version '
185 185 f'control with version {curr_version}')
186 186
187 187 except (RuntimeError, DatabaseNotControlledError):
188 188 curr_version = 1
189 189 msg = f'Current database is not under version control. ' \
190 190 f'Setting as version {curr_version}'
191 191 api.version_control(db_uri, repository_path, curr_version)
192 192
193 193 notify(msg)
194 194
195 195 if curr_version == __dbversion__:
196 196 log.info('This database is already at the newest version')
197 197 sys.exit(0)
198 198
199 199 upgrade_steps = list(range(curr_version + 1, __dbversion__ + 1))
200 200 notify(f'attempting to upgrade database from '
201 201 f'version {curr_version} to version {__dbversion__}')
202 202
203 203 # CALL THE PROPER ORDER OF STEPS TO PERFORM FULL UPGRADE
204 204 final_step = 'latest'
205 205 for step in upgrade_steps:
206 206 notify(f'performing upgrade step {step}')
207 207 time.sleep(0.5)
208 208
209 209 api.upgrade(db_uri, repository_path, step)
210 210 self.sa.rollback()
211 211 notify(f'schema upgrade for step {step} completed')
212 212
213 213 final_step = step
214 214
215 215 self.run_post_migration_tasks()
216 216 notify(f'upgrade to version {final_step} successful')
217 217
218 218 def fix_repo_paths(self):
219 219 """
220 220 Fixes an old RhodeCode version path into new one without a '*'
221 221 """
222 222
223 223 paths = self.sa.query(RhodeCodeUi)\
224 224 .filter(RhodeCodeUi.ui_key == '/')\
225 225 .scalar()
226 226
227 227 paths.ui_value = paths.ui_value.replace('*', '')
228 228
229 229 try:
230 230 self.sa.add(paths)
231 231 self.sa.commit()
232 232 except Exception:
233 233 self.sa.rollback()
234 234 raise
235 235
236 236 def fix_default_user(self):
237 237 """
238 238 Fixes an old default user with some 'nicer' default values,
239 239 used mostly for anonymous access
240 240 """
241 241 def_user = self.sa.query(User)\
242 242 .filter(User.username == User.DEFAULT_USER)\
243 243 .one()
244 244
245 245 def_user.name = 'Anonymous'
246 246 def_user.lastname = 'User'
247 247 def_user.email = User.DEFAULT_USER_EMAIL
248 248
249 249 try:
250 250 self.sa.add(def_user)
251 251 self.sa.commit()
252 252 except Exception:
253 253 self.sa.rollback()
254 254 raise
255 255
256 256 def fix_settings(self):
257 257 """
258 258 Fixes rhodecode settings and adds ga_code key for google analytics
259 259 """
260 260
261 261 hgsettings3 = RhodeCodeSetting('ga_code', '')
262 262
263 263 try:
264 264 self.sa.add(hgsettings3)
265 265 self.sa.commit()
266 266 except Exception:
267 267 self.sa.rollback()
268 268 raise
269 269
270 270 def create_admin_and_prompt(self):
271 271
272 272 # defaults
273 273 defaults = self.cli_args
274 274 username = defaults.get('username')
275 275 password = defaults.get('password')
276 276 email = defaults.get('email')
277 277
278 278 if username is None:
279 279 username = input('Specify admin username:')
280 280 if password is None:
281 281 password = self._get_admin_password()
282 282 if not password:
283 283 # second try
284 284 password = self._get_admin_password()
285 285 if not password:
286 286 sys.exit()
287 287 if email is None:
288 288 email = input('Specify admin email:')
289 289 api_key = self.cli_args.get('api_key')
290 290 self.create_user(username, password, email, True,
291 291 strict_creation_check=False,
292 292 api_key=api_key)
293 293
294 294 def _get_admin_password(self):
295 295 password = getpass.getpass('Specify admin password '
296 296 '(min 6 chars):')
297 297 confirm = getpass.getpass('Confirm password:')
298 298
299 299 if password != confirm:
300 300 log.error('passwords mismatch')
301 301 return False
302 302 if len(password) < 6:
303 303 log.error('password is too short - use at least 6 characters')
304 304 return False
305 305
306 306 return password
307 307
308 308 def create_test_admin_and_users(self):
309 309 log.info('creating admin and regular test users')
310 310 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, \
311 311 TEST_USER_ADMIN_PASS, TEST_USER_ADMIN_EMAIL, \
312 312 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, \
313 313 TEST_USER_REGULAR_EMAIL, TEST_USER_REGULAR2_LOGIN, \
314 314 TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR2_EMAIL
315 315
316 316 self.create_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
317 317 TEST_USER_ADMIN_EMAIL, True, api_key=True)
318 318
319 319 self.create_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
320 320 TEST_USER_REGULAR_EMAIL, False, api_key=True)
321 321
322 322 self.create_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS,
323 323 TEST_USER_REGULAR2_EMAIL, False, api_key=True)
324 324
325 325 def create_ui_settings(self, repo_store_path):
326 326 """
327 327 Creates ui settings, fills out hooks
328 328 and disables dotencode
329 329 """
330 330 settings_model = SettingsModel(sa=self.sa)
331 331 from rhodecode.lib.vcs.backends.hg import largefiles_store
332 332 from rhodecode.lib.vcs.backends.git import lfs_store
333 333
334 334 # Build HOOKS
335 335 hooks = [
336 336 (RhodeCodeUi.HOOK_REPO_SIZE, 'python:vcsserver.hooks.repo_size'),
337 337
338 338 # HG
339 339 (RhodeCodeUi.HOOK_PRE_PULL, 'python:vcsserver.hooks.pre_pull'),
340 340 (RhodeCodeUi.HOOK_PULL, 'python:vcsserver.hooks.log_pull_action'),
341 341 (RhodeCodeUi.HOOK_PRE_PUSH, 'python:vcsserver.hooks.pre_push'),
342 342 (RhodeCodeUi.HOOK_PRETX_PUSH, 'python:vcsserver.hooks.pre_push'),
343 343 (RhodeCodeUi.HOOK_PUSH, 'python:vcsserver.hooks.log_push_action'),
344 344 (RhodeCodeUi.HOOK_PUSH_KEY, 'python:vcsserver.hooks.key_push'),
345 345
346 346 ]
347 347
348 348 for key, value in hooks:
349 349 hook_obj = settings_model.get_ui_by_key(key)
350 350 hooks2 = hook_obj if hook_obj else RhodeCodeUi()
351 351 hooks2.ui_section = 'hooks'
352 352 hooks2.ui_key = key
353 353 hooks2.ui_value = value
354 354 self.sa.add(hooks2)
355 355
356 356 # enable largefiles
357 357 largefiles = RhodeCodeUi()
358 358 largefiles.ui_section = 'extensions'
359 359 largefiles.ui_key = 'largefiles'
360 360 largefiles.ui_value = ''
361 361 self.sa.add(largefiles)
362 362
363 363 # set default largefiles cache dir, defaults to
364 364 # /repo_store_location/.cache/largefiles
365 365 largefiles = RhodeCodeUi()
366 366 largefiles.ui_section = 'largefiles'
367 367 largefiles.ui_key = 'usercache'
368 368 largefiles.ui_value = largefiles_store(repo_store_path)
369 369
370 370 self.sa.add(largefiles)
371 371
372 372 # set default lfs cache dir, defaults to
373 373 # /repo_store_location/.cache/lfs_store
374 374 lfsstore = RhodeCodeUi()
375 375 lfsstore.ui_section = 'vcs_git_lfs'
376 376 lfsstore.ui_key = 'store_location'
377 377 lfsstore.ui_value = lfs_store(repo_store_path)
378 378
379 379 self.sa.add(lfsstore)
380 380
381 381 # enable hgevolve disabled by default
382 382 hgevolve = RhodeCodeUi()
383 383 hgevolve.ui_section = 'extensions'
384 384 hgevolve.ui_key = 'evolve'
385 385 hgevolve.ui_value = ''
386 386 hgevolve.ui_active = False
387 387 self.sa.add(hgevolve)
388 388
389 389 hgevolve = RhodeCodeUi()
390 390 hgevolve.ui_section = 'experimental'
391 391 hgevolve.ui_key = 'evolution'
392 392 hgevolve.ui_value = ''
393 393 hgevolve.ui_active = False
394 394 self.sa.add(hgevolve)
395 395
396 396 hgevolve = RhodeCodeUi()
397 397 hgevolve.ui_section = 'experimental'
398 398 hgevolve.ui_key = 'evolution.exchange'
399 399 hgevolve.ui_value = ''
400 400 hgevolve.ui_active = False
401 401 self.sa.add(hgevolve)
402 402
403 403 hgevolve = RhodeCodeUi()
404 404 hgevolve.ui_section = 'extensions'
405 405 hgevolve.ui_key = 'topic'
406 406 hgevolve.ui_value = ''
407 407 hgevolve.ui_active = False
408 408 self.sa.add(hgevolve)
409 409
410 410 # enable hggit disabled by default
411 411 hggit = RhodeCodeUi()
412 412 hggit.ui_section = 'extensions'
413 413 hggit.ui_key = 'hggit'
414 414 hggit.ui_value = ''
415 415 hggit.ui_active = False
416 416 self.sa.add(hggit)
417 417
418 418 # set svn branch defaults
419 419 branches = ["/branches/*", "/trunk"]
420 420 tags = ["/tags/*"]
421 421
422 422 for branch in branches:
423 423 settings_model.create_ui_section_value(
424 424 RhodeCodeUi.SVN_BRANCH_ID, branch)
425 425
426 426 for tag in tags:
427 427 settings_model.create_ui_section_value(RhodeCodeUi.SVN_TAG_ID, tag)
428 428
429 429 def create_auth_plugin_options(self, skip_existing=False):
430 430 """
431 431 Create default auth plugin settings, and make it active
432 432
433 433 :param skip_existing:
434 434 """
435 435 defaults = [
436 436 ('auth_plugins',
437 437 'egg:rhodecode-enterprise-ce#token,egg:rhodecode-enterprise-ce#rhodecode',
438 438 'list'),
439 439
440 440 ('auth_authtoken_enabled',
441 441 'True',
442 442 'bool'),
443 443
444 444 ('auth_rhodecode_enabled',
445 445 'True',
446 446 'bool'),
447 447 ]
448 448 for k, v, t in defaults:
449 449 if (skip_existing and
450 450 SettingsModel().get_setting_by_name(k) is not None):
451 451 log.debug('Skipping option %s', k)
452 452 continue
453 453 setting = RhodeCodeSetting(k, v, t)
454 454 self.sa.add(setting)
455 455
456 456 def create_default_options(self, skip_existing=False):
457 457 """Creates default settings"""
458 458
459 459 for k, v, t in [
460 460 ('default_repo_enable_locking', False, 'bool'),
461 461 ('default_repo_enable_downloads', False, 'bool'),
462 462 ('default_repo_enable_statistics', False, 'bool'),
463 463 ('default_repo_private', False, 'bool'),
464 464 ('default_repo_type', 'hg', 'unicode')]:
465 465
466 466 if (skip_existing and
467 467 SettingsModel().get_setting_by_name(k) is not None):
468 468 log.debug('Skipping option %s', k)
469 469 continue
470 470 setting = RhodeCodeSetting(k, v, t)
471 471 self.sa.add(setting)
472 472
473 473 def fixup_groups(self):
474 474 def_usr = User.get_default_user()
475 475 for g in RepoGroup.query().all():
476 476 g.group_name = g.get_new_name(g.name)
477 477 self.sa.add(g)
478 478 # get default perm
479 479 default = UserRepoGroupToPerm.query()\
480 480 .filter(UserRepoGroupToPerm.group == g)\
481 481 .filter(UserRepoGroupToPerm.user == def_usr)\
482 482 .scalar()
483 483
484 484 if default is None:
485 485 log.debug('missing default permission for group %s adding', g)
486 486 perm_obj = RepoGroupModel()._create_default_perms(g)
487 487 self.sa.add(perm_obj)
488 488
489 489 def reset_permissions(self, username):
490 490 """
491 491 Resets permissions to default state, useful when old systems had
492 492 bad permissions, we must clean them up
493 493
494 494 :param username:
495 495 """
496 496 default_user = User.get_by_username(username)
497 497 if not default_user:
498 498 return
499 499
500 500 u2p = UserToPerm.query()\
501 501 .filter(UserToPerm.user == default_user).all()
502 502 fixed = False
503 503 if len(u2p) != len(Permission.DEFAULT_USER_PERMISSIONS):
504 504 for p in u2p:
505 505 Session().delete(p)
506 506 fixed = True
507 507 self.populate_default_permissions()
508 508 return fixed
509 509
510 510 def config_prompt(self, test_repo_path='', retries=3):
511 511 defaults = self.cli_args
512 512 _path = defaults.get('repos_location')
513 513 if retries == 3:
514 514 log.info('Setting up repositories config')
515 515
516 516 if _path is not None:
517 517 path = _path
518 518 elif not self.tests and not test_repo_path:
519 519 path = input(
520 520 'Enter a valid absolute path to store repositories. '
521 521 'All repositories in that path will be added automatically:'
522 522 )
523 523 else:
524 524 path = test_repo_path
525 525 path_ok = True
526 526
527 527 # check proper dir
528 528 if not os.path.isdir(path):
529 529 path_ok = False
530 530 log.error('Given path %s is not a valid directory', path)
531 531
532 532 elif not os.path.isabs(path):
533 533 path_ok = False
534 534 log.error('Given path %s is not an absolute path', path)
535 535
536 536 # check if path is at least readable.
537 537 if not os.access(path, os.R_OK):
538 538 path_ok = False
539 539 log.error('Given path %s is not readable', path)
540 540
541 541 # check write access, warn user about non writeable paths
542 542 elif not os.access(path, os.W_OK) and path_ok:
543 543 log.warning('No write permission to given path %s', path)
544 544
545 545 q = (f'Given path {path} is not writeable, do you want to '
546 546 f'continue with read only mode ? [y/n]')
547 547 if not self.ask_ok(q):
548 548 log.error('Canceled by user')
549 549 sys.exit(-1)
550 550
551 551 if retries == 0:
552 552 sys.exit('max retries reached')
553 553 if not path_ok:
554 554 retries -= 1
555 555 return self.config_prompt(test_repo_path, retries)
556 556
557 557 real_path = os.path.normpath(os.path.realpath(path))
558 558
559 559 if real_path != os.path.normpath(path):
560 560 q = (f'Path looks like a symlink, RhodeCode Enterprise will store '
561 561 f'given path as {real_path} ? [y/n]')
562 562 if not self.ask_ok(q):
563 563 log.error('Canceled by user')
564 564 sys.exit(-1)
565 565
566 566 return real_path
567 567
568 568 def create_settings(self, path):
569 569
570 570 self.create_ui_settings(path)
571 571
572 572 ui_config = [
573 ('web', 'push_ssl', 'False'),
574 573 ('web', 'allow_archive', 'gz zip bz2'),
575 574 ('web', 'allow_push', '*'),
576 575 ('web', 'baseurl', '/'),
577 576 ('paths', '/', path),
578 577 ('phases', 'publish', 'True')
579 578 ]
580 579 for section, key, value in ui_config:
581 580 ui_conf = RhodeCodeUi()
582 581 setattr(ui_conf, 'ui_section', section)
583 582 setattr(ui_conf, 'ui_key', key)
584 583 setattr(ui_conf, 'ui_value', value)
585 584 self.sa.add(ui_conf)
586 585
587 586 # rhodecode app settings
588 587 settings = [
589 588 ('realm', 'RhodeCode', 'unicode'),
590 589 ('title', '', 'unicode'),
591 590 ('pre_code', '', 'unicode'),
592 591 ('post_code', '', 'unicode'),
593 592
594 593 # Visual
595 594 ('show_public_icon', True, 'bool'),
596 595 ('show_private_icon', True, 'bool'),
597 596 ('stylify_metatags', True, 'bool'),
598 597 ('dashboard_items', 100, 'int'),
599 598 ('admin_grid_items', 25, 'int'),
600 599
601 600 ('markup_renderer', 'markdown', 'unicode'),
602 601
603 602 ('repository_fields', True, 'bool'),
604 603 ('show_version', True, 'bool'),
605 604 ('show_revision_number', True, 'bool'),
606 605 ('show_sha_length', 12, 'int'),
607 606
608 607 ('use_gravatar', False, 'bool'),
609 608 ('gravatar_url', User.DEFAULT_GRAVATAR_URL, 'unicode'),
610 609
611 610 ('clone_uri_tmpl', Repository.DEFAULT_CLONE_URI, 'unicode'),
612 611 ('clone_uri_id_tmpl', Repository.DEFAULT_CLONE_URI_ID, 'unicode'),
613 612 ('clone_uri_ssh_tmpl', Repository.DEFAULT_CLONE_URI_SSH, 'unicode'),
614 613 ('support_url', '', 'unicode'),
615 614 ('update_url', RhodeCodeSetting.DEFAULT_UPDATE_URL, 'unicode'),
616 615
617 616 # VCS Settings
618 617 ('pr_merge_enabled', True, 'bool'),
619 618 ('use_outdated_comments', True, 'bool'),
620 619 ('diff_cache', True, 'bool'),
621 620 ]
622 621
623 622 for key, val, type_ in settings:
624 623 sett = RhodeCodeSetting(key, val, type_)
625 624 self.sa.add(sett)
626 625
627 626 self.create_auth_plugin_options()
628 627 self.create_default_options()
629 628
630 629 log.info('created ui config')
631 630
632 631 def create_user(self, username, password, email='', admin=False,
633 632 strict_creation_check=True, api_key=None):
634 633 log.info('creating user `%s`', username)
635 634 user = UserModel().create_or_update(
636 635 username, password, email, firstname='RhodeCode', lastname='Admin',
637 636 active=True, admin=admin, extern_type="rhodecode",
638 637 strict_creation_check=strict_creation_check)
639 638
640 639 if api_key:
641 640 log.info('setting a new default auth token for user `%s`', username)
642 641 UserModel().add_auth_token(
643 642 user=user, lifetime_minutes=-1,
644 643 role=UserModel.auth_token_role.ROLE_ALL,
645 644 description='BUILTIN TOKEN')
646 645
647 646 def create_default_user(self):
648 647 log.info('creating default user')
649 648 # create default user for handling default permissions.
650 649 user = UserModel().create_or_update(username=User.DEFAULT_USER,
651 650 password=str(uuid.uuid1())[:20],
652 651 email=User.DEFAULT_USER_EMAIL,
653 652 firstname='Anonymous',
654 653 lastname='User',
655 654 strict_creation_check=False)
656 655 # based on configuration options activate/de-activate this user which
657 656 # controls anonymous access
658 657 if self.cli_args.get('public_access') is False:
659 658 log.info('Public access disabled')
660 659 user.active = False
661 660 Session().add(user)
662 661 Session().commit()
663 662
664 663 def create_permissions(self):
665 664 """
666 665 Creates all permissions defined in the system
667 666 """
668 667 # module.(access|create|change|delete)_[name]
669 668 # module.(none|read|write|admin)
670 669 log.info('creating permissions')
671 670 PermissionModel(self.sa).create_permissions()
672 671
673 672 def populate_default_permissions(self):
674 673 """
675 674 Populate default permissions. It will create only the default
676 675 permissions that are missing, and not alter already defined ones
677 676 """
678 677 log.info('creating default user permissions')
679 678 PermissionModel(self.sa).create_default_user_permissions(user=User.DEFAULT_USER)
@@ -1,683 +1,662 b''
1 1
2 2
3 3 # Copyright (C) 2014-2023 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 SimpleVCS middleware for handling protocol request (push/clone etc.)
23 23 It's implemented with basic auth function
24 24 """
25 25
26 26 import os
27 27 import re
28 28 import logging
29 29 import importlib
30 30 from functools import wraps
31 31
32 32 import time
33 33 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
34 34
35 35 from pyramid.httpexceptions import (
36 36 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
37 37 from zope.cachedescriptors.property import Lazy as LazyProperty
38 38
39 39 import rhodecode
40 40 from rhodecode.authentication.base import authenticate, VCS_TYPE, loadplugin
41 41 from rhodecode.lib import rc_cache
42 42 from rhodecode.lib.svn_txn_utils import store_txn_id_data
43 43 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
44 44 from rhodecode.lib.base import (
45 45 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
46 46 from rhodecode.lib.exceptions import (UserCreationError, NotAllowedToCreateUserError)
47 47 from rhodecode.lib.hook_daemon.base import prepare_callback_daemon
48 48 from rhodecode.lib.middleware import appenlight
49 49 from rhodecode.lib.middleware.utils import scm_app_http
50 50 from rhodecode.lib.str_utils import safe_bytes, safe_int
51 51 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
52 52 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool
53 53 from rhodecode.lib.vcs.conf import settings as vcs_settings
54 54 from rhodecode.lib.vcs.backends import base
55 55
56 56 from rhodecode.model import meta
57 57 from rhodecode.model.db import User, Repository, PullRequest
58 58 from rhodecode.model.scm import ScmModel
59 59 from rhodecode.model.pull_request import PullRequestModel
60 60 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
61 61
62 62 log = logging.getLogger(__name__)
63 63
64 64
65 65 def initialize_generator(factory):
66 66 """
67 67 Initializes the returned generator by draining its first element.
68 68
69 69 This can be used to give a generator an initializer, which is the code
70 70 up to the first yield statement. This decorator enforces that the first
71 71 produced element has the value ``"__init__"`` to make its special
72 72 purpose very explicit in the using code.
73 73 """
74 74
75 75 @wraps(factory)
76 76 def wrapper(*args, **kwargs):
77 77 gen = factory(*args, **kwargs)
78 78 try:
79 79 init = next(gen)
80 80 except StopIteration:
81 81 raise ValueError('Generator must yield at least one element.')
82 82 if init != "__init__":
83 83 raise ValueError('First yielded element must be "__init__".')
84 84 return gen
85 85 return wrapper
86 86
87 87
88 88 class SimpleVCS(object):
89 89 """Common functionality for SCM HTTP handlers."""
90 90
91 91 SCM = 'unknown'
92 92
93 93 acl_repo_name = None
94 94 url_repo_name = None
95 95 vcs_repo_name = None
96 96 rc_extras = {}
97 97
98 98 # We have to handle requests to shadow repositories different than requests
99 99 # to normal repositories. Therefore we have to distinguish them. To do this
100 100 # we use this regex which will match only on URLs pointing to shadow
101 101 # repositories.
102 102 shadow_repo_re = re.compile(
103 103 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
104 104 '(?P<target>{slug_pat})/' # target repo
105 105 'pull-request/(?P<pr_id>\\d+)/' # pull request
106 106 'repository$' # shadow repo
107 107 .format(slug_pat=SLUG_RE.pattern))
108 108
109 109 def __init__(self, config, registry):
110 110 self.registry = registry
111 111 self.config = config
112 112 # re-populated by specialized middleware
113 113 self.repo_vcs_config = base.Config()
114 114
115 115 rc_settings = SettingsModel().get_all_settings(cache=True, from_request=False)
116 116 realm = rc_settings.get('rhodecode_realm') or 'RhodeCode AUTH'
117 117
118 118 # authenticate this VCS request using authfunc
119 119 auth_ret_code_detection = \
120 120 str2bool(self.config.get('auth_ret_code_detection', False))
121 121 self.authenticate = BasicAuth(
122 122 '', authenticate, registry, config.get('auth_ret_code'),
123 123 auth_ret_code_detection, rc_realm=realm)
124 124 self.ip_addr = '0.0.0.0'
125 125
126 126 @LazyProperty
127 127 def global_vcs_config(self):
128 128 try:
129 129 return VcsSettingsModel().get_ui_settings_as_config_obj()
130 130 except Exception:
131 131 return base.Config()
132 132
133 133 @property
134 134 def base_path(self):
135 135 settings_path = self.config.get('repo_store.path')
136 136
137 137 if not settings_path:
138 138 raise ValueError('FATAL: repo_store.path is empty')
139 139 return settings_path
140 140
141 141 def set_repo_names(self, environ):
142 142 """
143 143 This will populate the attributes acl_repo_name, url_repo_name,
144 144 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
145 145 shadow) repositories all names are equal. In case of requests to a
146 146 shadow repository the acl-name points to the target repo of the pull
147 147 request and the vcs-name points to the shadow repo file system path.
148 148 The url-name is always the URL used by the vcs client program.
149 149
150 150 Example in case of a shadow repo:
151 151 acl_repo_name = RepoGroup/MyRepo
152 152 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
153 153 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
154 154 """
155 155 # First we set the repo name from URL for all attributes. This is the
156 156 # default if handling normal (non shadow) repo requests.
157 157 self.url_repo_name = self._get_repository_name(environ)
158 158 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
159 159 self.is_shadow_repo = False
160 160
161 161 # Check if this is a request to a shadow repository.
162 162 match = self.shadow_repo_re.match(self.url_repo_name)
163 163 if match:
164 164 match_dict = match.groupdict()
165 165
166 166 # Build acl repo name from regex match.
167 167 acl_repo_name = safe_str('{groups}{target}'.format(
168 168 groups=match_dict['groups'] or '',
169 169 target=match_dict['target']))
170 170
171 171 # Retrieve pull request instance by ID from regex match.
172 172 pull_request = PullRequest.get(match_dict['pr_id'])
173 173
174 174 # Only proceed if we got a pull request and if acl repo name from
175 175 # URL equals the target repo name of the pull request.
176 176 if pull_request and (acl_repo_name == pull_request.target_repo.repo_name):
177 177
178 178 # Get file system path to shadow repository.
179 179 workspace_id = PullRequestModel()._workspace_id(pull_request)
180 180 vcs_repo_name = pull_request.target_repo.get_shadow_repository_path(workspace_id)
181 181
182 182 # Store names for later usage.
183 183 self.vcs_repo_name = vcs_repo_name
184 184 self.acl_repo_name = acl_repo_name
185 185 self.is_shadow_repo = True
186 186
187 187 log.debug('Setting all VCS repository names: %s', {
188 188 'acl_repo_name': self.acl_repo_name,
189 189 'url_repo_name': self.url_repo_name,
190 190 'vcs_repo_name': self.vcs_repo_name,
191 191 })
192 192
193 193 @property
194 194 def scm_app(self):
195 195 custom_implementation = self.config['vcs.scm_app_implementation']
196 196 if custom_implementation == 'http':
197 197 log.debug('Using HTTP implementation of scm app.')
198 198 scm_app_impl = scm_app_http
199 199 else:
200 200 log.debug('Using custom implementation of scm_app: "{}"'.format(
201 201 custom_implementation))
202 202 scm_app_impl = importlib.import_module(custom_implementation)
203 203 return scm_app_impl
204 204
205 205 def _get_by_id(self, repo_name):
206 206 """
207 207 Gets a special pattern _<ID> from clone url and tries to replace it
208 208 with a repository_name for support of _<ID> non changeable urls
209 209 """
210 210
211 211 data = repo_name.split('/')
212 212 if len(data) >= 2:
213 213 from rhodecode.model.repo import RepoModel
214 214 by_id_match = RepoModel().get_repo_by_id(repo_name)
215 215 if by_id_match:
216 216 data[1] = by_id_match.repo_name
217 217
218 218 # Because PEP-3333-WSGI uses bytes-tunneled-in-latin-1 as PATH_INFO
219 219 # and we use this data
220 220 maybe_new_path = '/'.join(data)
221 221 return safe_bytes(maybe_new_path).decode('latin1')
222 222
223 223 def _invalidate_cache(self, repo_name):
224 224 """
225 225 Set's cache for this repository for invalidation on next access
226 226
227 227 :param repo_name: full repo name, also a cache key
228 228 """
229 229 ScmModel().mark_for_invalidation(repo_name)
230 230
231 231 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
232 232 db_repo = Repository.get_by_repo_name(repo_name)
233 233 if not db_repo:
234 234 log.debug('Repository `%s` not found inside the database.',
235 235 repo_name)
236 236 return False
237 237
238 238 if db_repo.repo_type != scm_type:
239 239 log.warning(
240 240 'Repository `%s` have incorrect scm_type, expected %s got %s',
241 241 repo_name, db_repo.repo_type, scm_type)
242 242 return False
243 243
244 244 config = db_repo._config
245 245 config.set('extensions', 'largefiles', '')
246 246 return is_valid_repo(
247 247 repo_name, base_path,
248 248 explicit_scm=scm_type, expect_scm=scm_type, config=config)
249 249
250 250 def valid_and_active_user(self, user):
251 251 """
252 252 Checks if that user is not empty, and if it's actually object it checks
253 253 if he's active.
254 254
255 255 :param user: user object or None
256 256 :return: boolean
257 257 """
258 258 if user is None:
259 259 return False
260 260
261 261 elif user.active:
262 262 return True
263 263
264 264 return False
265 265
266 266 @property
267 267 def is_shadow_repo_dir(self):
268 268 return os.path.isdir(self.vcs_repo_name)
269 269
270 270 def _check_permission(self, action, user, auth_user, repo_name, ip_addr=None,
271 271 plugin_id='', plugin_cache_active=False, cache_ttl=0):
272 272 """
273 273 Checks permissions using action (push/pull) user and repository
274 274 name. If plugin_cache and ttl is set it will use the plugin which
275 275 authenticated the user to store the cached permissions result for N
276 276 amount of seconds as in cache_ttl
277 277
278 278 :param action: push or pull action
279 279 :param user: user instance
280 280 :param repo_name: repository name
281 281 """
282 282
283 283 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
284 284 plugin_id, plugin_cache_active, cache_ttl)
285 285
286 286 user_id = user.user_id
287 287 cache_namespace_uid = f'cache_user_auth.{rc_cache.PERMISSIONS_CACHE_VER}.{user_id}'
288 288 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
289 289
290 290 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
291 291 expiration_time=cache_ttl,
292 292 condition=plugin_cache_active)
293 293 def compute_perm_vcs(
294 294 cache_name, plugin_id, action, user_id, repo_name, ip_addr):
295 295
296 296 log.debug('auth: calculating permission access now...')
297 297 # check IP
298 298 inherit = user.inherit_default_permissions
299 299 ip_allowed = AuthUser.check_ip_allowed(
300 300 user_id, ip_addr, inherit_from_default=inherit)
301 301 if ip_allowed:
302 302 log.info('Access for IP:%s allowed', ip_addr)
303 303 else:
304 304 return False
305 305
306 306 if action == 'push':
307 307 perms = ('repository.write', 'repository.admin')
308 308 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
309 309 return False
310 310
311 311 else:
312 312 # any other action need at least read permission
313 313 perms = (
314 314 'repository.read', 'repository.write', 'repository.admin')
315 315 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
316 316 return False
317 317
318 318 return True
319 319
320 320 start = time.time()
321 321 log.debug('Running plugin `%s` permissions check', plugin_id)
322 322
323 323 # for environ based auth, password can be empty, but then the validation is
324 324 # on the server that fills in the env data needed for authentication
325 325 perm_result = compute_perm_vcs(
326 326 'vcs_permissions', plugin_id, action, user.user_id, repo_name, ip_addr)
327 327
328 328 auth_time = time.time() - start
329 329 log.debug('Permissions for plugin `%s` completed in %.4fs, '
330 330 'expiration time of fetched cache %.1fs.',
331 331 plugin_id, auth_time, cache_ttl)
332 332
333 333 return perm_result
334 334
335 335 def _get_http_scheme(self, environ):
336 336 try:
337 337 return environ['wsgi.url_scheme']
338 338 except Exception:
339 339 log.exception('Failed to read http scheme')
340 340 return 'http'
341 341
342 def _check_ssl(self, environ, start_response):
343 """
344 Checks the SSL check flag and returns False if SSL is not present
345 and required True otherwise
346 """
347 org_proto = environ['wsgi._org_proto']
348 # check if we have SSL required ! if not it's a bad request !
349 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
350 if require_ssl and org_proto == 'http':
351 log.debug(
352 'Bad request: detected protocol is `%s` and '
353 'SSL/HTTPS is required.', org_proto)
354 return False
355 return True
356
357 342 def _get_default_cache_ttl(self):
358 343 # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
359 344 plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode')
360 345 plugin_settings = plugin.get_settings()
361 346 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(
362 347 plugin_settings) or (False, 0)
363 348 return plugin_cache_active, cache_ttl
364 349
365 350 def __call__(self, environ, start_response):
366 351 try:
367 352 return self._handle_request(environ, start_response)
368 353 except Exception:
369 354 log.exception("Exception while handling request")
370 355 appenlight.track_exception(environ)
371 356 return HTTPInternalServerError()(environ, start_response)
372 357 finally:
373 358 meta.Session.remove()
374 359
375 360 def _handle_request(self, environ, start_response):
376 if not self._check_ssl(environ, start_response):
377 reason = ('SSL required, while RhodeCode was unable '
378 'to detect this as SSL request')
379 log.debug('User not allowed to proceed, %s', reason)
380 return HTTPNotAcceptable(reason)(environ, start_response)
381
382 361 if not self.url_repo_name:
383 362 log.warning('Repository name is empty: %s', self.url_repo_name)
384 363 # failed to get repo name, we fail now
385 364 return HTTPNotFound()(environ, start_response)
386 365 log.debug('Extracted repo name is %s', self.url_repo_name)
387 366
388 367 ip_addr = get_ip_addr(environ)
389 368 user_agent = get_user_agent(environ)
390 369 username = None
391 370
392 371 # skip passing error to error controller
393 372 environ['pylons.status_code_redirect'] = True
394 373
395 374 # ======================================================================
396 375 # GET ACTION PULL or PUSH
397 376 # ======================================================================
398 377 action = self._get_action(environ)
399 378
400 379 # ======================================================================
401 380 # Check if this is a request to a shadow repository of a pull request.
402 381 # In this case only pull action is allowed.
403 382 # ======================================================================
404 383 if self.is_shadow_repo and action != 'pull':
405 384 reason = 'Only pull action is allowed for shadow repositories.'
406 385 log.debug('User not allowed to proceed, %s', reason)
407 386 return HTTPNotAcceptable(reason)(environ, start_response)
408 387
409 388 # Check if the shadow repo actually exists, in case someone refers
410 389 # to it, and it has been deleted because of successful merge.
411 390 if self.is_shadow_repo and not self.is_shadow_repo_dir:
412 391 log.debug(
413 392 'Shadow repo detected, and shadow repo dir `%s` is missing',
414 393 self.is_shadow_repo_dir)
415 394 return HTTPNotFound()(environ, start_response)
416 395
417 396 # ======================================================================
418 397 # CHECK ANONYMOUS PERMISSION
419 398 # ======================================================================
420 399 detect_force_push = False
421 400 check_branch_perms = False
422 401 if action in ['pull', 'push']:
423 402 user_obj = anonymous_user = User.get_default_user()
424 403 auth_user = user_obj.AuthUser()
425 404 username = anonymous_user.username
426 405 if anonymous_user.active:
427 406 plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
428 407 # ONLY check permissions if the user is activated
429 408 anonymous_perm = self._check_permission(
430 409 action, anonymous_user, auth_user, self.acl_repo_name, ip_addr,
431 410 plugin_id='anonymous_access',
432 411 plugin_cache_active=plugin_cache_active,
433 412 cache_ttl=cache_ttl,
434 413 )
435 414 else:
436 415 anonymous_perm = False
437 416
438 417 if not anonymous_user.active or not anonymous_perm:
439 418 if not anonymous_user.active:
440 419 log.debug('Anonymous access is disabled, running '
441 420 'authentication')
442 421
443 422 if not anonymous_perm:
444 423 log.debug('Not enough credentials to access repo: `%s` '
445 424 'repository as anonymous user', self.acl_repo_name)
446 425
447 426 username = None
448 427 # ==============================================================
449 428 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
450 429 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
451 430 # ==============================================================
452 431
453 432 # try to auth based on environ, container auth methods
454 433 log.debug('Running PRE-AUTH for container|headers based authentication')
455 434
456 435 # headers auth, by just reading special headers and bypass the auth with user/passwd
457 436 pre_auth = authenticate(
458 437 '', '', environ, VCS_TYPE, registry=self.registry,
459 438 acl_repo_name=self.acl_repo_name)
460 439
461 440 if pre_auth and pre_auth.get('username'):
462 441 username = pre_auth['username']
463 442 log.debug('PRE-AUTH got `%s` as username', username)
464 443 if pre_auth:
465 444 log.debug('PRE-AUTH successful from %s',
466 445 pre_auth.get('auth_data', {}).get('_plugin'))
467 446
468 447 # If not authenticated by the container, running basic auth
469 448 # before inject the calling repo_name for special scope checks
470 449 self.authenticate.acl_repo_name = self.acl_repo_name
471 450
472 451 plugin_cache_active, cache_ttl = False, 0
473 452 plugin = None
474 453
475 454 # regular auth chain
476 455 if not username:
477 456 self.authenticate.realm = self.authenticate.get_rc_realm()
478 457
479 458 try:
480 459 auth_result = self.authenticate(environ)
481 460 except (UserCreationError, NotAllowedToCreateUserError) as e:
482 461 log.error(e)
483 462 reason = safe_str(e)
484 463 return HTTPNotAcceptable(reason)(environ, start_response)
485 464
486 465 if isinstance(auth_result, dict):
487 466 AUTH_TYPE.update(environ, 'basic')
488 467 REMOTE_USER.update(environ, auth_result['username'])
489 468 username = auth_result['username']
490 469 plugin = auth_result.get('auth_data', {}).get('_plugin')
491 470 log.info(
492 471 'MAIN-AUTH successful for user `%s` from %s plugin',
493 472 username, plugin)
494 473
495 474 plugin_cache_active, cache_ttl = auth_result.get(
496 475 'auth_data', {}).get('_ttl_cache') or (False, 0)
497 476 else:
498 477 return auth_result.wsgi_application(environ, start_response)
499 478
500 479 # ==============================================================
501 480 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
502 481 # ==============================================================
503 482 user = User.get_by_username(username)
504 483 if not self.valid_and_active_user(user):
505 484 return HTTPForbidden()(environ, start_response)
506 485 username = user.username
507 486 user_id = user.user_id
508 487
509 488 # check user attributes for password change flag
510 489 user_obj = user
511 490 auth_user = user_obj.AuthUser()
512 491 if user_obj and user_obj.username != User.DEFAULT_USER and \
513 492 user_obj.user_data.get('force_password_change'):
514 493 reason = 'password change required'
515 494 log.debug('User not allowed to authenticate, %s', reason)
516 495 return HTTPNotAcceptable(reason)(environ, start_response)
517 496
518 497 # check permissions for this repository
519 498 perm = self._check_permission(
520 499 action, user, auth_user, self.acl_repo_name, ip_addr,
521 500 plugin, plugin_cache_active, cache_ttl)
522 501 if not perm:
523 502 return HTTPForbidden()(environ, start_response)
524 503 environ['rc_auth_user_id'] = str(user_id)
525 504
526 505 if action == 'push':
527 506 perms = auth_user.get_branch_permissions(self.acl_repo_name)
528 507 if perms:
529 508 check_branch_perms = True
530 509 detect_force_push = True
531 510
532 511 # extras are injected into UI object and later available
533 512 # in hooks executed by RhodeCode
534 513 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
535 514
536 515 extras = vcs_operation_context(
537 516 environ, repo_name=self.acl_repo_name, username=username,
538 517 action=action, scm=self.SCM, check_locking=check_locking,
539 518 is_shadow_repo=self.is_shadow_repo, check_branch_perms=check_branch_perms,
540 519 detect_force_push=detect_force_push
541 520 )
542 521
543 522 # ======================================================================
544 523 # REQUEST HANDLING
545 524 # ======================================================================
546 525 repo_path = os.path.join(
547 526 safe_str(self.base_path), safe_str(self.vcs_repo_name))
548 527 log.debug('Repository path is %s', repo_path)
549 528
550 529 fix_PATH()
551 530
552 531 log.info(
553 532 '%s action on %s repo "%s" by "%s" from %s %s',
554 533 action, self.SCM, safe_str(self.url_repo_name),
555 534 safe_str(username), ip_addr, user_agent)
556 535
557 536 return self._generate_vcs_response(
558 537 environ, start_response, repo_path, extras, action)
559 538
560 539 def _get_txn_id(self, environ):
561 540
562 541 for k in ['RAW_URI', 'HTTP_DESTINATION']:
563 542 url = environ.get(k)
564 543 if not url:
565 544 continue
566 545
567 546 # regex to search for svn-txn-id
568 547 pattern = r'/!svn/txr/([^/]+)/'
569 548
570 549 # Search for the pattern in the URL
571 550 match = re.search(pattern, url)
572 551
573 552 # Check if a match is found and extract the captured group
574 553 if match:
575 554 txn_id = match.group(1)
576 555 return txn_id
577 556
578 557 @initialize_generator
579 558 def _generate_vcs_response(
580 559 self, environ, start_response, repo_path, extras, action):
581 560 """
582 561 Returns a generator for the response content.
583 562
584 563 This method is implemented as a generator, so that it can trigger
585 564 the cache validation after all content sent back to the client. It
586 565 also handles the locking exceptions which will be triggered when
587 566 the first chunk is produced by the underlying WSGI application.
588 567 """
589 568 svn_txn_id = ''
590 569 if action == 'push':
591 570 svn_txn_id = self._get_txn_id(environ)
592 571
593 572 callback_daemon, extras = self._prepare_callback_daemon(
594 573 extras, environ, action, txn_id=svn_txn_id)
595 574
596 575 if svn_txn_id:
597 576
598 577 port = safe_int(extras['hooks_uri'].split(':')[-1])
599 578 txn_id_data = extras.copy()
600 579 txn_id_data.update({'port': port})
601 580 txn_id_data.update({'req_method': environ['REQUEST_METHOD']})
602 581
603 582 full_repo_path = repo_path
604 583 store_txn_id_data(full_repo_path, svn_txn_id, txn_id_data)
605 584
606 585 log.debug('HOOKS extras is %s', extras)
607 586
608 587 http_scheme = self._get_http_scheme(environ)
609 588
610 589 config = self._create_config(extras, self.acl_repo_name, scheme=http_scheme)
611 590 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
612 591 with callback_daemon:
613 592 app.rc_extras = extras
614 593
615 594 try:
616 595 response = app(environ, start_response)
617 596 finally:
618 597 # This statement works together with the decorator
619 598 # "initialize_generator" above. The decorator ensures that
620 599 # we hit the first yield statement before the generator is
621 600 # returned back to the WSGI server. This is needed to
622 601 # ensure that the call to "app" above triggers the
623 602 # needed callback to "start_response" before the
624 603 # generator is actually used.
625 604 yield "__init__"
626 605
627 606 # iter content
628 607 for chunk in response:
629 608 yield chunk
630 609
631 610 try:
632 611 # invalidate cache on push
633 612 if action == 'push':
634 613 self._invalidate_cache(self.url_repo_name)
635 614 finally:
636 615 meta.Session.remove()
637 616
638 617 def _get_repository_name(self, environ):
639 618 """Get repository name out of the environmnent
640 619
641 620 :param environ: WSGI environment
642 621 """
643 622 raise NotImplementedError()
644 623
645 624 def _get_action(self, environ):
646 625 """Map request commands into a pull or push command.
647 626
648 627 :param environ: WSGI environment
649 628 """
650 629 raise NotImplementedError()
651 630
652 631 def _create_wsgi_app(self, repo_path, repo_name, config):
653 632 """Return the WSGI app that will finally handle the request."""
654 633 raise NotImplementedError()
655 634
656 635 def _create_config(self, extras, repo_name, scheme='http'):
657 636 """Create a safe config representation."""
658 637 raise NotImplementedError()
659 638
660 639 def _should_use_callback_daemon(self, extras, environ, action):
661 640 if extras.get('is_shadow_repo'):
662 641 # we don't want to execute hooks, and callback daemon for shadow repos
663 642 return False
664 643 return True
665 644
666 645 def _prepare_callback_daemon(self, extras, environ, action, txn_id=None):
667 646 protocol = vcs_settings.HOOKS_PROTOCOL
668 647
669 648 if not self._should_use_callback_daemon(extras, environ, action):
670 649 # disable callback daemon for actions that don't require it
671 650 protocol = 'local'
672 651
673 652 return prepare_callback_daemon(
674 653 extras, protocol=protocol,
675 654 host=vcs_settings.HOOKS_HOST, txn_id=txn_id)
676 655
677 656
678 657 def _should_check_locking(query_string):
679 658 # this is kind of hacky, but due to how mercurial handles client-server
680 659 # server see all operation on commit; bookmarks, phases and
681 660 # obsolescence marker in different transaction, we don't want to check
682 661 # locking on those
683 662 return query_string not in ['cmd=listkeys']
@@ -1,308 +1,312 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import gzip
21 21 import shutil
22 22 import logging
23 23 import tempfile
24 24 import urllib.parse
25 25
26 26 from webob.exc import HTTPNotFound
27 27
28 28 import rhodecode
29 29 from rhodecode.apps._base import ADMIN_PREFIX
30 30 from rhodecode.lib.middleware.utils import get_path_info
31 31 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
32 32 from rhodecode.lib.middleware.simplegit import SimpleGit, GIT_PROTO_PAT
33 33 from rhodecode.lib.middleware.simplehg import SimpleHg
34 34 from rhodecode.lib.middleware.simplesvn import SimpleSvn
35 35 from rhodecode.lib.str_utils import safe_str
36 36 from rhodecode.model.settings import VcsSettingsModel
37 37
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41 VCS_TYPE_KEY = '_rc_vcs_type'
42 42 VCS_TYPE_SKIP = '_rc_vcs_skip'
43 43
44 44
45 45 def is_git(environ):
46 46 """
47 47 Returns True if requests should be handled by GIT wsgi middleware
48 48 """
49 49 path_info = get_path_info(environ)
50 50 is_git_path = GIT_PROTO_PAT.match(path_info)
51 51 log.debug(
52 52 'request path: `%s` detected as GIT PROTOCOL %s', path_info,
53 53 is_git_path is not None)
54 54
55 55 return is_git_path
56 56
57 57
58 58 def is_hg(environ):
59 59 """
60 60 Returns True if requests target is mercurial server - header
61 61 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
62 62 """
63 63 is_hg_path = False
64 64
65 65 http_accept = environ.get('HTTP_ACCEPT')
66 66
67 67 if http_accept and http_accept.startswith('application/mercurial'):
68 68 query = urllib.parse.parse_qs(environ['QUERY_STRING'])
69 69 if 'cmd' in query:
70 70 is_hg_path = True
71 71
72 72 path_info = get_path_info(environ)
73 73 log.debug(
74 74 'request path: `%s` detected as HG PROTOCOL %s', path_info,
75 75 is_hg_path)
76 76
77 77 return is_hg_path
78 78
79 79
80 80 def is_svn(environ):
81 81 """
82 82 Returns True if requests target is Subversion server
83 83 """
84 84
85 85 http_dav = environ.get('HTTP_DAV', '')
86 86 magic_path_segment = rhodecode.CONFIG.get(
87 87 'rhodecode_subversion_magic_path', '/!svn')
88 88 path_info = get_path_info(environ)
89 89 req_method = environ['REQUEST_METHOD']
90 90
91 91 is_svn_path = (
92 92 'subversion' in http_dav or
93 93 magic_path_segment in path_info
94 94 or req_method in ['PROPFIND', 'PROPPATCH', 'HEAD']
95 95 )
96 96 log.debug(
97 97 'request path: `%s` detected as SVN PROTOCOL %s', path_info,
98 98 is_svn_path)
99 99
100 100 return is_svn_path
101 101
102 102
103 103 class GunzipMiddleware(object):
104 104 """
105 105 WSGI middleware that unzips gzip-encoded requests before
106 106 passing on to the underlying application.
107 107 """
108 108
109 109 def __init__(self, application):
110 110 self.app = application
111 111
112 112 def __call__(self, environ, start_response):
113 113 accepts_encoding_header = safe_str(environ.get('HTTP_CONTENT_ENCODING', ''))
114 114
115 115 if 'gzip' in accepts_encoding_header:
116 116 log.debug('gzip detected, now running gunzip wrapper')
117 117 wsgi_input = environ['wsgi.input']
118 118
119 119 if not hasattr(environ['wsgi.input'], 'seek'):
120 120 # The gzip implementation in the standard library of Python 2.x
121 121 # requires the '.seek()' and '.tell()' methods to be available
122 122 # on the input stream. Read the data into a temporary file to
123 123 # work around this limitation.
124 124
125 125 wsgi_input = tempfile.SpooledTemporaryFile(64 * 1024 * 1024)
126 126 shutil.copyfileobj(environ['wsgi.input'], wsgi_input)
127 127 wsgi_input.seek(0)
128 128
129 129 environ['wsgi.input'] = gzip.GzipFile(fileobj=wsgi_input, mode='r')
130 130 # since we "Ungzipped" the content we say now it's no longer gzip
131 131 # content encoding
132 132 del environ['HTTP_CONTENT_ENCODING']
133 133
134 134 # content length has changes ? or i'm not sure
135 135 if 'CONTENT_LENGTH' in environ:
136 136 del environ['CONTENT_LENGTH']
137 137 else:
138 138 log.debug('content not gzipped, gzipMiddleware passing '
139 139 'request further')
140 140 return self.app(environ, start_response)
141 141
142 142
143 143 def is_vcs_call(environ):
144 144 if VCS_TYPE_KEY in environ:
145 145 raw_type = environ[VCS_TYPE_KEY]
146 146 return raw_type and raw_type != VCS_TYPE_SKIP
147 147 return False
148 148
149 149
150 150 def detect_vcs_request(environ, backends):
151 151 checks = {
152 152 'hg': (is_hg, SimpleHg),
153 153 'git': (is_git, SimpleGit),
154 154 'svn': (is_svn, SimpleSvn),
155 155 }
156 156 handler = None
157 157 # List of path views first chunk we don't do any checks
158 158 white_list = [
159 159 # favicon often requested by browsers
160 160 'favicon.ico',
161 161
162 # static files no detection
163 '_static++',
164
165 # debug-toolbar
166 '_debug_toolbar++',
167
162 168 # e.g /_file_store/download
163 169 '_file_store++',
164 170
165 171 # login
166 "_admin/login",
172 f"{ADMIN_PREFIX}/login",
173 f"{ADMIN_PREFIX}/logout",
167 174
168 175 # 2fa
169 176 f"{ADMIN_PREFIX}/check_2fa",
170 177 f"{ADMIN_PREFIX}/setup_2fa",
171 178
172 179 # _admin/api is safe too
173 180 f'{ADMIN_PREFIX}/api',
174 181
175 182 # _admin/gist is safe too
176 183 f'{ADMIN_PREFIX}/gists++',
177 184
178 185 # _admin/my_account is safe too
179 186 f'{ADMIN_PREFIX}/my_account++',
180 187
181 # static files no detection
182 '_static++',
183
184 # debug-toolbar
185 '_debug_toolbar++',
186
187 188 # skip ops ping, status
188 189 f'{ADMIN_PREFIX}/ops/ping',
189 190 f'{ADMIN_PREFIX}/ops/status',
190 191
191 192 # full channelstream connect should be VCS skipped
192 193 f'{ADMIN_PREFIX}/channelstream/connect',
193 194
194 195 '++/repo_creating_check'
195 196 ]
197
196 198 path_info = get_path_info(environ)
197 199 path_url = path_info.lstrip('/')
198 200 req_method = environ.get('REQUEST_METHOD')
199 201
200 202 for item in white_list:
203 item = item.lstrip('/')
204
201 205 if item.endswith('++') and path_url.startswith(item[:-2]):
202 206 log.debug('path `%s` in whitelist (match:%s), skipping...', path_url, item)
203 207 return handler
204 208 if item.startswith('++') and path_url.endswith(item[2:]):
205 209 log.debug('path `%s` in whitelist (match:%s), skipping...', path_url, item)
206 210 return handler
207 211 if item == path_url:
208 212 log.debug('path `%s` in whitelist (match:%s), skipping...', path_url, item)
209 213 return handler
210 214
211 215 if VCS_TYPE_KEY in environ:
212 216 raw_type = environ[VCS_TYPE_KEY]
213 217 if raw_type == VCS_TYPE_SKIP:
214 218 log.debug('got `skip` marker for vcs detection, skipping...')
215 219 return handler
216 220
217 221 _check, handler = checks.get(raw_type) or [None, None]
218 222 if handler:
219 223 log.debug('got handler:%s from environ', handler)
220 224
221 225 if not handler:
222 226 log.debug('request start: checking if request for `%s:%s` is of VCS type in order: %s',
223 227 req_method, path_url, backends)
224 228 for vcs_type in backends:
225 229 vcs_check, _handler = checks[vcs_type]
226 230 if vcs_check(environ):
227 231 log.debug('vcs handler found %s', _handler)
228 232 handler = _handler
229 233 break
230 234
231 235 return handler
232 236
233 237
234 238 class VCSMiddleware(object):
235 239
236 240 def __init__(self, app, registry, config, appenlight_client):
237 241 self.application = app
238 242 self.registry = registry
239 243 self.config = config
240 244 self.appenlight_client = appenlight_client
241 245 self.use_gzip = True
242 246 # order in which we check the middlewares, based on vcs.backends config
243 247 self.check_middlewares = config['vcs.backends']
244 248
245 249 def vcs_config(self, repo_name=None):
246 250 """
247 251 returns serialized VcsSettings
248 252 """
249 253 try:
250 254 return VcsSettingsModel(
251 255 repo=repo_name).get_ui_settings_as_config_obj()
252 256 except Exception:
253 257 pass
254 258
255 259 def wrap_in_gzip_if_enabled(self, app, config):
256 260 if self.use_gzip:
257 261 app = GunzipMiddleware(app)
258 262 return app
259 263
260 264 def _get_handler_app(self, environ):
261 265 app = None
262 266 log.debug('VCSMiddleware: detecting vcs type.')
263 267 handler = detect_vcs_request(environ, self.check_middlewares)
264 268 if handler:
265 269 app = handler(self.config, self.registry)
266 270
267 271 return app
268 272
269 273 def __call__(self, environ, start_response):
270 274 # check if we handle one of interesting protocols, optionally extract
271 275 # specific vcsSettings and allow changes of how things are wrapped
272 276 vcs_handler = self._get_handler_app(environ)
273 277 if vcs_handler:
274 278 # translate the _REPO_ID into real repo NAME for usage
275 279 # in middleware
276 280
277 281 path_info = get_path_info(environ)
278 282 environ['PATH_INFO'] = vcs_handler._get_by_id(path_info)
279 283
280 284 # Set acl, url and vcs repo names.
281 285 vcs_handler.set_repo_names(environ)
282 286
283 287 # register repo config back to the handler
284 288 vcs_conf = self.vcs_config(vcs_handler.acl_repo_name)
285 289 # maybe damaged/non existent settings. We still want to
286 290 # pass that point to validate on is_valid_and_existing_repo
287 291 # and return proper HTTP Code back to client
288 292 if vcs_conf:
289 293 vcs_handler.repo_vcs_config = vcs_conf
290 294
291 295 # check for type, presence in database and on filesystem
292 296 if not vcs_handler.is_valid_and_existing_repo(
293 297 vcs_handler.acl_repo_name,
294 298 vcs_handler.base_path,
295 299 vcs_handler.SCM):
296 300 return HTTPNotFound()(environ, start_response)
297 301
298 302 environ['REPO_NAME'] = vcs_handler.url_repo_name
299 303
300 304 # Wrap handler in middlewares if they are enabled.
301 305 vcs_handler = self.wrap_in_gzip_if_enabled(
302 306 vcs_handler, self.config)
303 307 vcs_handler, _ = wrap_in_appenlight_if_enabled(
304 308 vcs_handler, self.config, self.appenlight_client)
305 309
306 310 return vcs_handler(environ, start_response)
307 311
308 312 return self.application(environ, start_response)
@@ -1,124 +1,123 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18 import logging
19 19
20 20 import click
21 21 import pyramid.paster
22 22
23 23 from rhodecode.lib.pyramid_utils import bootstrap
24 24 from rhodecode.lib.config_utils import get_app_config
25 25 from rhodecode.lib.db_manage import DbManage
26 26 from rhodecode.lib.utils2 import get_encryption_key
27 27 from rhodecode.model.db import Session
28 28
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32
33 33 @click.command()
34 34 @click.argument('ini_path', type=click.Path(exists=True))
35 35 @click.option(
36 36 '--force-yes/--force-no', default=None,
37 37 help="Force yes/no to every question")
38 38 @click.option(
39 39 '--user',
40 40 default=None,
41 41 help='Initial super-admin username')
42 42 @click.option(
43 43 '--email',
44 44 default=None,
45 45 help='Initial super-admin email address.')
46 46 @click.option(
47 47 '--password',
48 48 default=None,
49 49 help='Initial super-admin password. Minimum 6 chars.')
50 50 @click.option(
51 51 '--api-key',
52 52 help='Initial API key for the admin user')
53 53 @click.option(
54 54 '--repos',
55 55 default=None,
56 56 help='Absolute path to storage location. This is storage for all '
57 57 'existing and future repositories, and repository groups.')
58 58 @click.option(
59 59 '--public-access/--no-public-access',
60 60 default=None,
61 61 help='Enable public access on this installation. '
62 62 'Default is public access enabled.')
63 63 @click.option(
64 64 '--skip-existing-db',
65 65 default=False,
66 66 is_flag=True,
67 67 help='Do not destroy and re-initialize the database if it already exist.')
68 68 @click.option(
69 69 '--apply-license-key',
70 70 default=False,
71 71 is_flag=True,
72 72 help='Get the license key from a license file or ENV and apply during DB creation.')
73 73 def main(ini_path, force_yes, user, email, password, api_key, repos,
74 74 public_access, skip_existing_db, apply_license_key):
75 75 return command(ini_path, force_yes, user, email, password, api_key,
76 76 repos, public_access, skip_existing_db, apply_license_key)
77 77
78 78
79 79 def command(ini_path, force_yes, user, email, password, api_key, repos,
80 80 public_access, skip_existing_db, apply_license_key):
81 81 # mapping of old parameters to new CLI from click
82 82 options = dict(
83 83 username=user,
84 84 email=email,
85 85 password=password,
86 86 api_key=api_key,
87 87 repos_location=repos,
88 88 force_ask=force_yes,
89 89 public_access=public_access
90 90 )
91 91 pyramid.paster.setup_logging(ini_path)
92 92
93 93 config = get_app_config(ini_path)
94 94
95 95 db_uri = config['sqlalchemy.db1.url']
96 96 enc_key = get_encryption_key(config)
97 97 dbmanage = DbManage(log_sql=True, dbconf=db_uri, root='.',
98 98 tests=False, cli_args=options, enc_key=enc_key)
99 99 if skip_existing_db and dbmanage.db_exists():
100 100 return
101 101
102 102 dbmanage.create_tables(override=True)
103 103 dbmanage.set_db_version()
104 104 opts = dbmanage.config_prompt(None)
105 105 dbmanage.create_settings(opts)
106 106 dbmanage.create_default_user()
107 107 dbmanage.create_admin_and_prompt()
108 108 dbmanage.create_permissions()
109 109 dbmanage.populate_default_permissions()
110 110 if apply_license_key:
111 try:
112 from rc_license.models import apply_trial_license_if_missing
113 apply_trial_license_if_missing(force=True)
114 except ImportError:
115 pass
111 from rhodecode.model.license import apply_license_from_file
112 license_file_path = config.get('license.import_path')
113 if license_file_path:
114 apply_license_from_file(license_file_path, force=True)
116 115
117 116 Session().commit()
118 117
119 118 with bootstrap(ini_path, env={'RC_CMD_SETUP_RC': '1'}) as env:
120 119 msg = 'Successfully initialized database, schema and default data.'
121 120 print()
122 121 print('*' * len(msg))
123 122 print(msg.upper())
124 123 print('*' * len(msg))
@@ -1,835 +1,834 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 Utilities library for RhodeCode
21 21 """
22 22
23 23 import datetime
24 24
25 25 import decorator
26 26 import logging
27 27 import os
28 28 import re
29 29 import sys
30 30 import shutil
31 31 import socket
32 32 import tempfile
33 33 import traceback
34 34 import tarfile
35 35
36 36 from functools import wraps
37 37 from os.path import join as jn
38 38
39 39 import paste
40 40 import pkg_resources
41 41 from webhelpers2.text import collapse, strip_tags, convert_accented_entities, convert_misc_entities
42 42
43 43 from mako import exceptions
44 44
45 45 from rhodecode import ConfigGet
46 46 from rhodecode.lib.hash_utils import sha256_safe, md5, sha1
47 47 from rhodecode.lib.type_utils import AttributeDict
48 48 from rhodecode.lib.str_utils import safe_bytes, safe_str
49 49 from rhodecode.lib.vcs.backends.base import Config
50 50 from rhodecode.lib.vcs.exceptions import VCSError
51 51 from rhodecode.lib.vcs.utils.helpers import get_scm, get_scm_backend
52 52 from rhodecode.lib.ext_json import sjson as json
53 53 from rhodecode.model import meta
54 54 from rhodecode.model.db import (
55 55 Repository, User, RhodeCodeUi, UserLog, RepoGroup, UserGroup)
56 56 from rhodecode.model.meta import Session
57 57
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
62 62
63 63 # String which contains characters that are not allowed in slug names for
64 64 # repositories or repository groups. It is properly escaped to use it in
65 65 # regular expressions.
66 66 SLUG_BAD_CHARS = re.escape(r'`?=[]\;\'"<>,/~!@#$%^&*()+{}|:')
67 67
68 68 # Regex that matches forbidden characters in repo/group slugs.
69 69 SLUG_BAD_CHAR_RE = re.compile(r'[{}\x00-\x08\x0b-\x0c\x0e-\x1f]'.format(SLUG_BAD_CHARS))
70 70
71 71 # Regex that matches allowed characters in repo/group slugs.
72 72 SLUG_GOOD_CHAR_RE = re.compile(r'[^{}]'.format(SLUG_BAD_CHARS))
73 73
74 74 # Regex that matches whole repo/group slugs.
75 75 SLUG_RE = re.compile(r'[^{}]+'.format(SLUG_BAD_CHARS))
76 76
77 77 _license_cache = None
78 78
79 79
80 80 def adopt_for_celery(func):
81 81 """
82 82 Decorator designed to adopt hooks (from rhodecode.lib.hooks_base)
83 83 for further usage as a celery tasks.
84 84 """
85 85 @wraps(func)
86 86 def wrapper(extras):
87 87 extras = AttributeDict(extras)
88 88 try:
89 89 # HooksResponse implements to_json method which must be used there.
90 90 return func(extras).to_json()
91 91 except Exception as e:
92 92 return {'status': 128, 'exception': type(e).__name__, 'exception_args': e.args}
93 93 return wrapper
94 94
95 95
96 96 def repo_name_slug(value):
97 97 """
98 98 Return slug of name of repository
99 99 This function is called on each creation/modification
100 100 of repository to prevent bad names in repo
101 101 """
102 102
103 103 replacement_char = '-'
104 104
105 105 slug = strip_tags(value)
106 106 slug = convert_accented_entities(slug)
107 107 slug = convert_misc_entities(slug)
108 108
109 109 slug = SLUG_BAD_CHAR_RE.sub('', slug)
110 110 slug = re.sub(r'[\s]+', '-', slug)
111 111 slug = collapse(slug, replacement_char)
112 112
113 113 return slug
114 114
115 115
116 116 #==============================================================================
117 117 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
118 118 #==============================================================================
119 119 def get_repo_slug(request):
120 120 _repo = ''
121 121
122 122 if hasattr(request, 'db_repo_name'):
123 123 # if our requests has set db reference use it for name, this
124 124 # translates the example.com/_<id> into proper repo names
125 125 _repo = request.db_repo_name
126 126 elif getattr(request, 'matchdict', None):
127 127 # pyramid
128 128 _repo = request.matchdict.get('repo_name')
129 129
130 130 if _repo:
131 131 _repo = _repo.rstrip('/')
132 132 return _repo
133 133
134 134
135 135 def get_repo_group_slug(request):
136 136 _group = ''
137 137 if hasattr(request, 'db_repo_group'):
138 138 # if our requests has set db reference use it for name, this
139 139 # translates the example.com/_<id> into proper repo group names
140 140 _group = request.db_repo_group.group_name
141 141 elif getattr(request, 'matchdict', None):
142 142 # pyramid
143 143 _group = request.matchdict.get('repo_group_name')
144 144
145 145 if _group:
146 146 _group = _group.rstrip('/')
147 147 return _group
148 148
149 149
150 150 def get_user_group_slug(request):
151 151 _user_group = ''
152 152
153 153 if hasattr(request, 'db_user_group'):
154 154 _user_group = request.db_user_group.users_group_name
155 155 elif getattr(request, 'matchdict', None):
156 156 # pyramid
157 157 _user_group = request.matchdict.get('user_group_id')
158 158 _user_group_name = request.matchdict.get('user_group_name')
159 159 try:
160 160 if _user_group:
161 161 _user_group = UserGroup.get(_user_group)
162 162 elif _user_group_name:
163 163 _user_group = UserGroup.get_by_group_name(_user_group_name)
164 164
165 165 if _user_group:
166 166 _user_group = _user_group.users_group_name
167 167 except Exception:
168 168 log.exception('Failed to get user group by id and name')
169 169 # catch all failures here
170 170 return None
171 171
172 172 return _user_group
173 173
174 174
175 175 def get_filesystem_repos(path, recursive=False, skip_removed_repos=True):
176 176 """
177 177 Scans given path for repos and return (name,(type,path)) tuple
178 178
179 179 :param path: path to scan for repositories
180 180 :param recursive: recursive search and return names with subdirs in front
181 181 """
182 182
183 183 # remove ending slash for better results
184 184 path = path.rstrip(os.sep)
185 185 log.debug('now scanning in %s location recursive:%s...', path, recursive)
186 186
187 187 def _get_repos(p):
188 188 dirpaths = get_dirpaths(p)
189 189 if not _is_dir_writable(p):
190 190 log.warning('repo path without write access: %s', p)
191 191
192 192 for dirpath in dirpaths:
193 193 if os.path.isfile(os.path.join(p, dirpath)):
194 194 continue
195 195 cur_path = os.path.join(p, dirpath)
196 196
197 197 # skip removed repos
198 198 if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
199 199 continue
200 200
201 201 #skip .<somethin> dirs
202 202 if dirpath.startswith('.'):
203 203 continue
204 204
205 205 try:
206 206 scm_info = get_scm(cur_path)
207 207 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
208 208 except VCSError:
209 209 if not recursive:
210 210 continue
211 211 #check if this dir containts other repos for recursive scan
212 212 rec_path = os.path.join(p, dirpath)
213 213 if os.path.isdir(rec_path):
214 214 yield from _get_repos(rec_path)
215 215
216 216 return _get_repos(path)
217 217
218 218
219 219 def get_dirpaths(p: str) -> list:
220 220 try:
221 221 # OS-independable way of checking if we have at least read-only
222 222 # access or not.
223 223 dirpaths = os.listdir(p)
224 224 except OSError:
225 225 log.warning('ignoring repo path without read access: %s', p)
226 226 return []
227 227
228 228 # os.listpath has a tweak: If a unicode is passed into it, then it tries to
229 229 # decode paths and suddenly returns unicode objects itself. The items it
230 230 # cannot decode are returned as strings and cause issues.
231 231 #
232 232 # Those paths are ignored here until a solid solution for path handling has
233 233 # been built.
234 234 expected_type = type(p)
235 235
236 236 def _has_correct_type(item):
237 237 if type(item) is not expected_type:
238 238 log.error(
239 239 "Ignoring path %s since it cannot be decoded into str.",
240 240 # Using "repr" to make sure that we see the byte value in case
241 241 # of support.
242 242 repr(item))
243 243 return False
244 244 return True
245 245
246 246 dirpaths = [item for item in dirpaths if _has_correct_type(item)]
247 247
248 248 return dirpaths
249 249
250 250
251 251 def _is_dir_writable(path):
252 252 """
253 253 Probe if `path` is writable.
254 254
255 255 Due to trouble on Cygwin / Windows, this is actually probing if it is
256 256 possible to create a file inside of `path`, stat does not produce reliable
257 257 results in this case.
258 258 """
259 259 try:
260 260 with tempfile.TemporaryFile(dir=path):
261 261 pass
262 262 except OSError:
263 263 return False
264 264 return True
265 265
266 266
267 267 def is_valid_repo(repo_name, base_path, expect_scm=None, explicit_scm=None, config=None):
268 268 """
269 269 Returns True if given path is a valid repository False otherwise.
270 270 If expect_scm param is given also, compare if given scm is the same
271 271 as expected from scm parameter. If explicit_scm is given don't try to
272 272 detect the scm, just use the given one to check if repo is valid
273 273
274 274 :param repo_name:
275 275 :param base_path:
276 276 :param expect_scm:
277 277 :param explicit_scm:
278 278 :param config:
279 279
280 280 :return True: if given path is a valid repository
281 281 """
282 282 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
283 283 log.debug('Checking if `%s` is a valid path for repository. '
284 284 'Explicit type: %s', repo_name, explicit_scm)
285 285
286 286 try:
287 287 if explicit_scm:
288 288 detected_scms = [get_scm_backend(explicit_scm)(
289 289 full_path, config=config).alias]
290 290 else:
291 291 detected_scms = get_scm(full_path)
292 292
293 293 if expect_scm:
294 294 return detected_scms[0] == expect_scm
295 295 log.debug('path: %s is an vcs object:%s', full_path, detected_scms)
296 296 return True
297 297 except VCSError:
298 298 log.debug('path: %s is not a valid repo !', full_path)
299 299 return False
300 300
301 301
302 302 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
303 303 """
304 304 Returns True if a given path is a repository group, False otherwise
305 305
306 306 :param repo_group_name:
307 307 :param base_path:
308 308 """
309 309 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
310 310 log.debug('Checking if `%s` is a valid path for repository group',
311 311 repo_group_name)
312 312
313 313 # check if it's not a repo
314 314 if is_valid_repo(repo_group_name, base_path):
315 315 log.debug('Repo called %s exist, it is not a valid repo group', repo_group_name)
316 316 return False
317 317
318 318 try:
319 319 # we need to check bare git repos at higher level
320 320 # since we might match branches/hooks/info/objects or possible
321 321 # other things inside bare git repo
322 322 maybe_repo = os.path.dirname(full_path)
323 323 if maybe_repo == base_path:
324 324 # skip root level repo check; we know root location CANNOT BE a repo group
325 325 return False
326 326
327 327 scm_ = get_scm(maybe_repo)
328 328 log.debug('path: %s is a vcs object:%s, not valid repo group', full_path, scm_)
329 329 return False
330 330 except VCSError:
331 331 pass
332 332
333 333 # check if it's a valid path
334 334 if skip_path_check or os.path.isdir(full_path):
335 335 log.debug('path: %s is a valid repo group !', full_path)
336 336 return True
337 337
338 338 log.debug('path: %s is not a valid repo group !', full_path)
339 339 return False
340 340
341 341
342 342 def ask_ok(prompt, retries=4, complaint='[y]es or [n]o please!'):
343 343 while True:
344 344 ok = input(prompt)
345 345 if ok.lower() in ('y', 'ye', 'yes'):
346 346 return True
347 347 if ok.lower() in ('n', 'no', 'nop', 'nope'):
348 348 return False
349 349 retries = retries - 1
350 350 if retries < 0:
351 351 raise OSError
352 352 print(complaint)
353 353
354 354 # propagated from mercurial documentation
355 355 ui_sections = [
356 356 'alias', 'auth',
357 357 'decode/encode', 'defaults',
358 358 'diff', 'email',
359 359 'extensions', 'format',
360 360 'merge-patterns', 'merge-tools',
361 361 'hooks', 'http_proxy',
362 362 'smtp', 'patch',
363 363 'paths', 'profiling',
364 364 'server', 'trusted',
365 365 'ui', 'web', ]
366 366
367 367
368 368 def prepare_config_data(clear_session=True, repo=None):
369 369 """
370 370 Read the configuration data from the database, *.ini files and return configuration
371 371 tuples.
372 372 """
373 373 from rhodecode.model.settings import VcsSettingsModel
374 374
375 375 config = []
376 376
377 377 sa = meta.Session()
378 378 settings_model = VcsSettingsModel(repo=repo, sa=sa)
379 379
380 380 ui_settings = settings_model.get_ui_settings()
381 381
382 382 ui_data = []
383 383 for setting in ui_settings:
384 384 # Todo: remove this section once transition to *.ini files will be completed
385 385 if setting.section in ('largefiles', 'vcs_git_lfs'):
386 386 if setting.key != 'enabled':
387 387 continue
388 388 if setting.active:
389 389 ui_data.append((setting.section, setting.key, setting.value))
390 390 config.append((
391 391 safe_str(setting.section), safe_str(setting.key),
392 392 safe_str(setting.value)))
393 393 if setting.key == 'push_ssl':
394 # force set push_ssl requirement to False, rhodecode
395 # handles that
394 # force set push_ssl requirement to False this is deprecated, and we must force it to False
396 395 config.append((
397 396 safe_str(setting.section), safe_str(setting.key), False))
398 397 config_getter = ConfigGet()
399 398 config.append(('vcs_git_lfs', 'store_location', config_getter.get_str('vcs.git.lfs.storage_location')))
400 399 config.append(('largefiles', 'usercache', config_getter.get_str('vcs.hg.largefiles.storage_location')))
401 400 log.debug(
402 401 'settings ui from db@repo[%s]: %s',
403 402 repo,
404 403 ','.join(['[{}] {}={}'.format(*s) for s in ui_data]))
405 404 if clear_session:
406 405 meta.Session.remove()
407 406
408 407 # TODO: mikhail: probably it makes no sense to re-read hooks information.
409 408 # It's already there and activated/deactivated
410 409 skip_entries = []
411 410 enabled_hook_classes = get_enabled_hook_classes(ui_settings)
412 411 if 'pull' not in enabled_hook_classes:
413 412 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PULL))
414 413 if 'push' not in enabled_hook_classes:
415 414 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PUSH))
416 415 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRETX_PUSH))
417 416 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PUSH_KEY))
418 417
419 418 config = [entry for entry in config if entry[:2] not in skip_entries]
420 419
421 420 return config
422 421
423 422
424 423 def make_db_config(clear_session=True, repo=None):
425 424 """
426 425 Create a :class:`Config` instance based on the values in the database.
427 426 """
428 427 config = Config()
429 428 config_data = prepare_config_data(clear_session=clear_session, repo=repo)
430 429 for section, option, value in config_data:
431 430 config.set(section, option, value)
432 431 return config
433 432
434 433
435 434 def get_enabled_hook_classes(ui_settings):
436 435 """
437 436 Return the enabled hook classes.
438 437
439 438 :param ui_settings: List of ui_settings as returned
440 439 by :meth:`VcsSettingsModel.get_ui_settings`
441 440
442 441 :return: a list with the enabled hook classes. The order is not guaranteed.
443 442 :rtype: list
444 443 """
445 444 enabled_hooks = []
446 445 active_hook_keys = [
447 446 key for section, key, value, active in ui_settings
448 447 if section == 'hooks' and active]
449 448
450 449 hook_names = {
451 450 RhodeCodeUi.HOOK_PUSH: 'push',
452 451 RhodeCodeUi.HOOK_PULL: 'pull',
453 452 RhodeCodeUi.HOOK_REPO_SIZE: 'repo_size'
454 453 }
455 454
456 455 for key in active_hook_keys:
457 456 hook = hook_names.get(key)
458 457 if hook:
459 458 enabled_hooks.append(hook)
460 459
461 460 return enabled_hooks
462 461
463 462
464 463 def set_rhodecode_config(config):
465 464 """
466 465 Updates pyramid config with new settings from database
467 466
468 467 :param config:
469 468 """
470 469 from rhodecode.model.settings import SettingsModel
471 470 app_settings = SettingsModel().get_all_settings()
472 471
473 472 for k, v in list(app_settings.items()):
474 473 config[k] = v
475 474
476 475
477 476 def get_rhodecode_realm():
478 477 """
479 478 Return the rhodecode realm from database.
480 479 """
481 480 from rhodecode.model.settings import SettingsModel
482 481 realm = SettingsModel().get_setting_by_name('realm')
483 482 return safe_str(realm.app_settings_value)
484 483
485 484
486 485 def get_rhodecode_repo_store_path():
487 486 """
488 487 Returns the base path. The base path is the filesystem path which points
489 488 to the repository store.
490 489 """
491 490
492 491 import rhodecode
493 492 return rhodecode.CONFIG['repo_store.path']
494 493
495 494
496 495 def map_groups(path):
497 496 """
498 497 Given a full path to a repository, create all nested groups that this
499 498 repo is inside. This function creates parent-child relationships between
500 499 groups and creates default perms for all new groups.
501 500
502 501 :param paths: full path to repository
503 502 """
504 503 from rhodecode.model.repo_group import RepoGroupModel
505 504 sa = meta.Session()
506 505 groups = path.split(Repository.NAME_SEP)
507 506 parent = None
508 507 group = None
509 508
510 509 # last element is repo in nested groups structure
511 510 groups = groups[:-1]
512 511 rgm = RepoGroupModel(sa)
513 512 owner = User.get_first_super_admin()
514 513 for lvl, group_name in enumerate(groups):
515 514 group_name = '/'.join(groups[:lvl] + [group_name])
516 515 group = RepoGroup.get_by_group_name(group_name)
517 516 desc = '%s group' % group_name
518 517
519 518 # skip folders that are now removed repos
520 519 if REMOVED_REPO_PAT.match(group_name):
521 520 break
522 521
523 522 if group is None:
524 523 log.debug('creating group level: %s group_name: %s',
525 524 lvl, group_name)
526 525 group = RepoGroup(group_name, parent)
527 526 group.group_description = desc
528 527 group.user = owner
529 528 sa.add(group)
530 529 perm_obj = rgm._create_default_perms(group)
531 530 sa.add(perm_obj)
532 531 sa.flush()
533 532
534 533 parent = group
535 534 return group
536 535
537 536
538 537 def repo2db_mapper(initial_repo_list, remove_obsolete=False, force_hooks_rebuild=False):
539 538 """
540 539 maps all repos given in initial_repo_list, non existing repositories
541 540 are created, if remove_obsolete is True it also checks for db entries
542 541 that are not in initial_repo_list and removes them.
543 542
544 543 :param initial_repo_list: list of repositories found by scanning methods
545 544 :param remove_obsolete: check for obsolete entries in database
546 545 """
547 546 from rhodecode.model.repo import RepoModel
548 547 from rhodecode.model.repo_group import RepoGroupModel
549 548 from rhodecode.model.settings import SettingsModel
550 549
551 550 sa = meta.Session()
552 551 repo_model = RepoModel()
553 552 user = User.get_first_super_admin()
554 553 added = []
555 554
556 555 # creation defaults
557 556 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
558 557 enable_statistics = defs.get('repo_enable_statistics')
559 558 enable_locking = defs.get('repo_enable_locking')
560 559 enable_downloads = defs.get('repo_enable_downloads')
561 560 private = defs.get('repo_private')
562 561
563 562 for name, repo in list(initial_repo_list.items()):
564 563 group = map_groups(name)
565 564 str_name = safe_str(name)
566 565 db_repo = repo_model.get_by_repo_name(str_name)
567 566
568 567 # found repo that is on filesystem not in RhodeCode database
569 568 if not db_repo:
570 569 log.info('repository `%s` not found in the database, creating now', name)
571 570 added.append(name)
572 571 desc = (repo.description
573 572 if repo.description != 'unknown'
574 573 else '%s repository' % name)
575 574
576 575 db_repo = repo_model._create_repo(
577 576 repo_name=name,
578 577 repo_type=repo.alias,
579 578 description=desc,
580 579 repo_group=getattr(group, 'group_id', None),
581 580 owner=user,
582 581 enable_locking=enable_locking,
583 582 enable_downloads=enable_downloads,
584 583 enable_statistics=enable_statistics,
585 584 private=private,
586 585 state=Repository.STATE_CREATED
587 586 )
588 587 sa.commit()
589 588 # we added that repo just now, and make sure we updated server info
590 589 if db_repo.repo_type == 'git':
591 590 git_repo = db_repo.scm_instance()
592 591 # update repository server-info
593 592 log.debug('Running update server info')
594 593 git_repo._update_server_info(force=True)
595 594
596 595 db_repo.update_commit_cache(recursive=False)
597 596
598 597 config = db_repo._config
599 598 config.set('extensions', 'largefiles', '')
600 599 repo = db_repo.scm_instance(config=config)
601 600 repo.install_hooks(force=force_hooks_rebuild)
602 601
603 602 removed = []
604 603 if remove_obsolete:
605 604 # remove from database those repositories that are not in the filesystem
606 605 for repo in sa.query(Repository).all():
607 606 if repo.repo_name not in list(initial_repo_list.keys()):
608 607 log.debug("Removing non-existing repository found in db `%s`",
609 608 repo.repo_name)
610 609 try:
611 610 RepoModel(sa).delete(repo, forks='detach', fs_remove=False)
612 611 sa.commit()
613 612 removed.append(repo.repo_name)
614 613 except Exception:
615 614 # don't hold further removals on error
616 615 log.error(traceback.format_exc())
617 616 sa.rollback()
618 617
619 618 def splitter(full_repo_name):
620 619 _parts = full_repo_name.rsplit(RepoGroup.url_sep(), 1)
621 620 gr_name = None
622 621 if len(_parts) == 2:
623 622 gr_name = _parts[0]
624 623 return gr_name
625 624
626 625 initial_repo_group_list = [splitter(x) for x in
627 626 list(initial_repo_list.keys()) if splitter(x)]
628 627
629 628 # remove from database those repository groups that are not in the
630 629 # filesystem due to parent child relationships we need to delete them
631 630 # in a specific order of most nested first
632 631 all_groups = [x.group_name for x in sa.query(RepoGroup).all()]
633 632 def nested_sort(gr):
634 633 return len(gr.split('/'))
635 634 for group_name in sorted(all_groups, key=nested_sort, reverse=True):
636 635 if group_name not in initial_repo_group_list:
637 636 repo_group = RepoGroup.get_by_group_name(group_name)
638 637 if (repo_group.children.all() or
639 638 not RepoGroupModel().check_exist_filesystem(
640 639 group_name=group_name, exc_on_failure=False)):
641 640 continue
642 641
643 642 log.info(
644 643 'Removing non-existing repository group found in db `%s`',
645 644 group_name)
646 645 try:
647 646 RepoGroupModel(sa).delete(group_name, fs_remove=False)
648 647 sa.commit()
649 648 removed.append(group_name)
650 649 except Exception:
651 650 # don't hold further removals on error
652 651 log.exception(
653 652 'Unable to remove repository group `%s`',
654 653 group_name)
655 654 sa.rollback()
656 655 raise
657 656
658 657 return added, removed
659 658
660 659
661 660 def load_rcextensions(root_path):
662 661 import rhodecode
663 662 from rhodecode.config import conf
664 663
665 664 path = os.path.join(root_path)
666 665 sys.path.append(path)
667 666
668 667 try:
669 668 rcextensions = __import__('rcextensions')
670 669 except ImportError:
671 670 if os.path.isdir(os.path.join(path, 'rcextensions')):
672 671 log.warning('Unable to load rcextensions from %s', path)
673 672 rcextensions = None
674 673
675 674 if rcextensions:
676 675 log.info('Loaded rcextensions from %s...', rcextensions)
677 676 rhodecode.EXTENSIONS = rcextensions
678 677
679 678 # Additional mappings that are not present in the pygments lexers
680 679 conf.LANGUAGES_EXTENSIONS_MAP.update(
681 680 getattr(rhodecode.EXTENSIONS, 'EXTRA_MAPPINGS', {}))
682 681
683 682
684 683 def get_custom_lexer(extension):
685 684 """
686 685 returns a custom lexer if it is defined in rcextensions module, or None
687 686 if there's no custom lexer defined
688 687 """
689 688 import rhodecode
690 689 from pygments import lexers
691 690
692 691 # custom override made by RhodeCode
693 692 if extension in ['mako']:
694 693 return lexers.get_lexer_by_name('html+mako')
695 694
696 695 # check if we didn't define this extension as other lexer
697 696 extensions = rhodecode.EXTENSIONS and getattr(rhodecode.EXTENSIONS, 'EXTRA_LEXERS', None)
698 697 if extensions and extension in rhodecode.EXTENSIONS.EXTRA_LEXERS:
699 698 _lexer_name = rhodecode.EXTENSIONS.EXTRA_LEXERS[extension]
700 699 return lexers.get_lexer_by_name(_lexer_name)
701 700
702 701
703 702 #==============================================================================
704 703 # TEST FUNCTIONS AND CREATORS
705 704 #==============================================================================
706 705 def create_test_index(repo_location, config):
707 706 """
708 707 Makes default test index.
709 708 """
710 709 try:
711 710 import rc_testdata
712 711 except ImportError:
713 712 raise ImportError('Failed to import rc_testdata, '
714 713 'please make sure this package is installed from requirements_test.txt')
715 714 rc_testdata.extract_search_index(
716 715 'vcs_search_index', os.path.dirname(config['search.location']))
717 716
718 717
719 718 def create_test_directory(test_path):
720 719 """
721 720 Create test directory if it doesn't exist.
722 721 """
723 722 if not os.path.isdir(test_path):
724 723 log.debug('Creating testdir %s', test_path)
725 724 os.makedirs(test_path)
726 725
727 726
728 727 def create_test_database(test_path, config):
729 728 """
730 729 Makes a fresh database.
731 730 """
732 731 from rhodecode.lib.db_manage import DbManage
733 732 from rhodecode.lib.utils2 import get_encryption_key
734 733
735 734 # PART ONE create db
736 735 dbconf = config['sqlalchemy.db1.url']
737 736 enc_key = get_encryption_key(config)
738 737
739 738 log.debug('making test db %s', dbconf)
740 739
741 740 dbmanage = DbManage(log_sql=False, dbconf=dbconf, root=config['here'],
742 741 tests=True, cli_args={'force_ask': True}, enc_key=enc_key)
743 742 dbmanage.create_tables(override=True)
744 743 dbmanage.set_db_version()
745 744 # for tests dynamically set new root paths based on generated content
746 745 dbmanage.create_settings(dbmanage.config_prompt(test_path))
747 746 dbmanage.create_default_user()
748 747 dbmanage.create_test_admin_and_users()
749 748 dbmanage.create_permissions()
750 749 dbmanage.populate_default_permissions()
751 750 Session().commit()
752 751
753 752
754 753 def create_test_repositories(test_path, config):
755 754 """
756 755 Creates test repositories in the temporary directory. Repositories are
757 756 extracted from archives within the rc_testdata package.
758 757 """
759 758 import rc_testdata
760 759 from rhodecode.tests import HG_REPO, GIT_REPO, SVN_REPO
761 760
762 761 log.debug('making test vcs repositories')
763 762
764 763 idx_path = config['search.location']
765 764 data_path = config['cache_dir']
766 765
767 766 # clean index and data
768 767 if idx_path and os.path.exists(idx_path):
769 768 log.debug('remove %s', idx_path)
770 769 shutil.rmtree(idx_path)
771 770
772 771 if data_path and os.path.exists(data_path):
773 772 log.debug('remove %s', data_path)
774 773 shutil.rmtree(data_path)
775 774
776 775 rc_testdata.extract_hg_dump('vcs_test_hg', jn(test_path, HG_REPO))
777 776 rc_testdata.extract_git_dump('vcs_test_git', jn(test_path, GIT_REPO))
778 777
779 778 # Note: Subversion is in the process of being integrated with the system,
780 779 # until we have a properly packed version of the test svn repository, this
781 780 # tries to copy over the repo from a package "rc_testdata"
782 781 svn_repo_path = rc_testdata.get_svn_repo_archive()
783 782 with tarfile.open(svn_repo_path) as tar:
784 783 tar.extractall(jn(test_path, SVN_REPO))
785 784
786 785
787 786 def password_changed(auth_user, session):
788 787 # Never report password change in case of default user or anonymous user.
789 788 if auth_user.username == User.DEFAULT_USER or auth_user.user_id is None:
790 789 return False
791 790
792 791 password_hash = md5(safe_bytes(auth_user.password)) if auth_user.password else None
793 792 rhodecode_user = session.get('rhodecode_user', {})
794 793 session_password_hash = rhodecode_user.get('password', '')
795 794 return password_hash != session_password_hash
796 795
797 796
798 797 def read_opensource_licenses():
799 798 global _license_cache
800 799
801 800 if not _license_cache:
802 801 licenses = pkg_resources.resource_string(
803 802 'rhodecode', 'config/licenses.json')
804 803 _license_cache = json.loads(licenses)
805 804
806 805 return _license_cache
807 806
808 807
809 808 def generate_platform_uuid():
810 809 """
811 810 Generates platform UUID based on it's name
812 811 """
813 812 import platform
814 813
815 814 try:
816 815 uuid_list = [platform.platform()]
817 816 return sha256_safe(':'.join(uuid_list))
818 817 except Exception as e:
819 818 log.error('Failed to generate host uuid: %s', e)
820 819 return 'UNDEFINED'
821 820
822 821
823 822 def send_test_email(recipients, email_body='TEST EMAIL'):
824 823 """
825 824 Simple code for generating test emails.
826 825 Usage::
827 826
828 827 from rhodecode.lib import utils
829 828 utils.send_test_email()
830 829 """
831 830 from rhodecode.lib.celerylib import tasks, run_task
832 831
833 832 email_body = email_body_plaintext = email_body
834 833 subject = f'SUBJECT FROM: {socket.gethostname()}'
835 834 tasks.send_email(recipients, subject, email_body_plaintext, email_body)
@@ -1,663 +1,662 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 this is forms validation classes
21 21 http://formencode.org/module-formencode.validators.html
22 22 for list off all availible validators
23 23
24 24 we can create our own validators
25 25
26 26 The table below outlines the options which can be used in a schema in addition to the validators themselves
27 27 pre_validators [] These validators will be applied before the schema
28 28 chained_validators [] These validators will be applied after the schema
29 29 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
30 30 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
31 31 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
32 32 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
33 33
34 34
35 35 <name> = formencode.validators.<name of validator>
36 36 <name> must equal form name
37 37 list=[1,2,3,4,5]
38 38 for SELECT use formencode.All(OneOf(list), Int())
39 39
40 40 """
41 41
42 42 import deform
43 43 import logging
44 44 import formencode
45 45
46 46 from pkg_resources import resource_filename
47 47 from formencode import All, Pipe
48 48
49 49 from pyramid.threadlocal import get_current_request
50 50
51 51 from rhodecode import BACKENDS
52 52 from rhodecode.lib import helpers
53 53 from rhodecode.model import validators as v
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 deform_templates = resource_filename('deform', 'templates')
59 59 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
60 60 search_path = (rhodecode_templates, deform_templates)
61 61
62 62
63 63 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
64 64 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
65 65 def __call__(self, template_name, **kw):
66 66 kw['h'] = helpers
67 67 kw['request'] = get_current_request()
68 68 return self.load(template_name)(**kw)
69 69
70 70
71 71 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
72 72 deform.Form.set_default_renderer(form_renderer)
73 73
74 74
75 75 def LoginForm(localizer):
76 76 _ = localizer
77 77
78 78 class _LoginForm(formencode.Schema):
79 79 allow_extra_fields = True
80 80 filter_extra_fields = True
81 81 username = v.UnicodeString(
82 82 strip=True,
83 83 min=1,
84 84 not_empty=True,
85 85 messages={
86 86 'empty': _('Please enter a login'),
87 87 'tooShort': _('Enter a value %(min)i characters long or more')
88 88 }
89 89 )
90 90
91 91 password = v.UnicodeString(
92 92 strip=False,
93 93 min=3,
94 94 max=72,
95 95 not_empty=True,
96 96 messages={
97 97 'empty': _('Please enter a password'),
98 98 'tooShort': _('Enter %(min)i characters or more')}
99 99 )
100 100
101 101 remember = v.StringBoolean(if_missing=False)
102 102
103 103 chained_validators = [v.ValidAuth(localizer)]
104 104 return _LoginForm
105 105
106 106
107 107 def TOTPForm(localizer, user, allow_recovery_code_use=False):
108 108 _ = localizer
109 109
110 110 class _TOTPForm(formencode.Schema):
111 111 allow_extra_fields = True
112 112 filter_extra_fields = False
113 113 totp = v.Regex(r'^(?:\d{6}|[A-Z0-9]{32})$')
114 114 secret_totp = v.String()
115 115
116 116 def to_python(self, value, state=None):
117 117 validation_checks = [user.is_totp_valid]
118 118 if allow_recovery_code_use:
119 119 validation_checks.append(user.is_2fa_recovery_code_valid)
120 120 form_data = super().to_python(value, state)
121 121 received_code = form_data['totp']
122 122 secret = form_data.get('secret_totp')
123 123
124 124 if not any(map(lambda func: func(received_code, secret), validation_checks)):
125 125 error_msg = _('Code is invalid. Try again!')
126 126 raise formencode.Invalid(error_msg, v, state, error_dict={'totp': error_msg})
127 127 return form_data
128 128
129 129 return _TOTPForm
130 130
131 131
132 132 def WhitelistedVcsClientsForm(localizer):
133 133 _ = localizer
134 134
135 135 class _WhitelistedVcsClientsForm(formencode.Schema):
136 136 regexp = r'^(?:\s*[<>=~^!]*\s*\d{1,2}\.\d{1,2}(?:\.\d{1,2})?\s*|\*)\s*(?:,\s*[<>=~^!]*\s*\d{1,2}\.\d{1,2}(?:\.\d{1,2})?\s*|\s*\*\s*)*$'
137 137 allow_extra_fields = True
138 138 filter_extra_fields = True
139 139 git = v.Regex(regexp)
140 140 hg = v.Regex(regexp)
141 141 svn = v.Regex(regexp)
142 142
143 143 return _WhitelistedVcsClientsForm
144 144
145 145
146 146 def UserForm(localizer, edit=False, available_languages=None, old_data=None):
147 147 old_data = old_data or {}
148 148 available_languages = available_languages or []
149 149 _ = localizer
150 150
151 151 class _UserForm(formencode.Schema):
152 152 allow_extra_fields = True
153 153 filter_extra_fields = True
154 154 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
155 155 v.ValidUsername(localizer, edit, old_data))
156 156 if edit:
157 157 new_password = All(
158 158 v.ValidPassword(localizer),
159 159 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
160 160 )
161 161 password_confirmation = All(
162 162 v.ValidPassword(localizer),
163 163 v.UnicodeString(strip=False, min=6, max=72, not_empty=False),
164 164 )
165 165 admin = v.StringBoolean(if_missing=False)
166 166 else:
167 167 password = All(
168 168 v.ValidPassword(localizer),
169 169 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
170 170 )
171 171 password_confirmation = All(
172 172 v.ValidPassword(localizer),
173 173 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
174 174 )
175 175
176 176 password_change = v.StringBoolean(if_missing=False)
177 177 create_repo_group = v.StringBoolean(if_missing=False)
178 178
179 179 active = v.StringBoolean(if_missing=False)
180 180 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
181 181 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
182 182 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
183 183 description = v.UnicodeString(strip=True, min=1, max=250, not_empty=False,
184 184 if_missing='')
185 185 extern_name = v.UnicodeString(strip=True)
186 186 extern_type = v.UnicodeString(strip=True)
187 187 language = v.OneOf(available_languages, hideList=False,
188 188 testValueList=True, if_missing=None)
189 189 chained_validators = [v.ValidPasswordsMatch(localizer)]
190 190 return _UserForm
191 191
192 192
193 193 def UserGroupForm(localizer, edit=False, old_data=None, allow_disabled=False):
194 194 old_data = old_data or {}
195 195 _ = localizer
196 196
197 197 class _UserGroupForm(formencode.Schema):
198 198 allow_extra_fields = True
199 199 filter_extra_fields = True
200 200
201 201 users_group_name = All(
202 202 v.UnicodeString(strip=True, min=1, not_empty=True),
203 203 v.ValidUserGroup(localizer, edit, old_data)
204 204 )
205 205 user_group_description = v.UnicodeString(strip=True, min=1,
206 206 not_empty=False)
207 207
208 208 users_group_active = v.StringBoolean(if_missing=False)
209 209
210 210 if edit:
211 211 # this is user group owner
212 212 user = All(
213 213 v.UnicodeString(not_empty=True),
214 214 v.ValidRepoUser(localizer, allow_disabled))
215 215 return _UserGroupForm
216 216
217 217
218 218 def RepoGroupForm(localizer, edit=False, old_data=None, available_groups=None,
219 219 can_create_in_root=False, allow_disabled=False):
220 220 _ = localizer
221 221 old_data = old_data or {}
222 222 available_groups = available_groups or []
223 223
224 224 class _RepoGroupForm(formencode.Schema):
225 225 allow_extra_fields = True
226 226 filter_extra_fields = False
227 227
228 228 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
229 229 v.SlugifyName(localizer),)
230 230 group_description = v.UnicodeString(strip=True, min=1,
231 231 not_empty=False)
232 232 group_copy_permissions = v.StringBoolean(if_missing=False)
233 233
234 234 group_parent_id = v.OneOf(available_groups, hideList=False,
235 235 testValueList=True, not_empty=True)
236 236 enable_locking = v.StringBoolean(if_missing=False)
237 237 chained_validators = [
238 238 v.ValidRepoGroup(localizer, edit, old_data, can_create_in_root)]
239 239
240 240 if edit:
241 241 # this is repo group owner
242 242 user = All(
243 243 v.UnicodeString(not_empty=True),
244 244 v.ValidRepoUser(localizer, allow_disabled))
245 245 return _RepoGroupForm
246 246
247 247
248 248 def RegisterForm(localizer, edit=False, old_data=None):
249 249 _ = localizer
250 250 old_data = old_data or {}
251 251
252 252 class _RegisterForm(formencode.Schema):
253 253 allow_extra_fields = True
254 254 filter_extra_fields = True
255 255 username = All(
256 256 v.ValidUsername(localizer, edit, old_data),
257 257 v.UnicodeString(strip=True, min=1, not_empty=True)
258 258 )
259 259 password = All(
260 260 v.ValidPassword(localizer),
261 261 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
262 262 )
263 263 password_confirmation = All(
264 264 v.ValidPassword(localizer),
265 265 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
266 266 )
267 267 active = v.StringBoolean(if_missing=False)
268 268 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
269 269 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
270 270 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
271 271
272 272 chained_validators = [v.ValidPasswordsMatch(localizer)]
273 273 return _RegisterForm
274 274
275 275
276 276 def PasswordResetForm(localizer):
277 277 _ = localizer
278 278
279 279 class _PasswordResetForm(formencode.Schema):
280 280 allow_extra_fields = True
281 281 filter_extra_fields = True
282 282 email = All(v.ValidSystemEmail(localizer), v.Email(not_empty=True))
283 283 return _PasswordResetForm
284 284
285 285
286 286 def RepoForm(localizer, edit=False, old_data=None, repo_groups=None, allow_disabled=False):
287 287 _ = localizer
288 288 old_data = old_data or {}
289 289 repo_groups = repo_groups or []
290 290 supported_backends = BACKENDS.keys()
291 291
292 292 class _RepoForm(formencode.Schema):
293 293 allow_extra_fields = True
294 294 filter_extra_fields = False
295 295 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
296 296 v.SlugifyName(localizer), v.CannotHaveGitSuffix(localizer))
297 297 repo_group = All(v.CanWriteGroup(localizer, old_data),
298 298 v.OneOf(repo_groups, hideList=True))
299 299 repo_type = v.OneOf(supported_backends, required=False,
300 300 if_missing=old_data.get('repo_type'))
301 301 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
302 302 repo_private = v.StringBoolean(if_missing=False)
303 303 repo_copy_permissions = v.StringBoolean(if_missing=False)
304 304 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
305 305
306 306 repo_enable_statistics = v.StringBoolean(if_missing=False)
307 307 repo_enable_downloads = v.StringBoolean(if_missing=False)
308 308 repo_enable_locking = v.StringBoolean(if_missing=False)
309 309
310 310 if edit:
311 311 # this is repo owner
312 312 user = All(
313 313 v.UnicodeString(not_empty=True),
314 314 v.ValidRepoUser(localizer, allow_disabled))
315 315 clone_uri_change = v.UnicodeString(
316 316 not_empty=False, if_missing=v.Missing)
317 317
318 318 chained_validators = [v.ValidCloneUri(localizer),
319 319 v.ValidRepoName(localizer, edit, old_data)]
320 320 return _RepoForm
321 321
322 322
323 323 def RepoPermsForm(localizer):
324 324 _ = localizer
325 325
326 326 class _RepoPermsForm(formencode.Schema):
327 327 allow_extra_fields = True
328 328 filter_extra_fields = False
329 329 chained_validators = [v.ValidPerms(localizer, type_='repo')]
330 330 return _RepoPermsForm
331 331
332 332
333 333 def RepoGroupPermsForm(localizer, valid_recursive_choices):
334 334 _ = localizer
335 335
336 336 class _RepoGroupPermsForm(formencode.Schema):
337 337 allow_extra_fields = True
338 338 filter_extra_fields = False
339 339 recursive = v.OneOf(valid_recursive_choices)
340 340 chained_validators = [v.ValidPerms(localizer, type_='repo_group')]
341 341 return _RepoGroupPermsForm
342 342
343 343
344 344 def UserGroupPermsForm(localizer):
345 345 _ = localizer
346 346
347 347 class _UserPermsForm(formencode.Schema):
348 348 allow_extra_fields = True
349 349 filter_extra_fields = False
350 350 chained_validators = [v.ValidPerms(localizer, type_='user_group')]
351 351 return _UserPermsForm
352 352
353 353
354 354 def RepoFieldForm(localizer):
355 355 _ = localizer
356 356
357 357 class _RepoFieldForm(formencode.Schema):
358 358 filter_extra_fields = True
359 359 allow_extra_fields = True
360 360
361 361 new_field_key = All(v.FieldKey(localizer),
362 362 v.UnicodeString(strip=True, min=3, not_empty=True))
363 363 new_field_value = v.UnicodeString(not_empty=False, if_missing='')
364 364 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
365 365 if_missing='str')
366 366 new_field_label = v.UnicodeString(not_empty=False)
367 367 new_field_desc = v.UnicodeString(not_empty=False)
368 368 return _RepoFieldForm
369 369
370 370
371 371 def RepoForkForm(localizer, edit=False, old_data=None,
372 372 supported_backends=BACKENDS.keys(), repo_groups=None):
373 373 _ = localizer
374 374 old_data = old_data or {}
375 375 repo_groups = repo_groups or []
376 376
377 377 class _RepoForkForm(formencode.Schema):
378 378 allow_extra_fields = True
379 379 filter_extra_fields = False
380 380 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
381 381 v.SlugifyName(localizer))
382 382 repo_group = All(v.CanWriteGroup(localizer, ),
383 383 v.OneOf(repo_groups, hideList=True))
384 384 repo_type = All(v.ValidForkType(localizer, old_data), v.OneOf(supported_backends))
385 385 description = v.UnicodeString(strip=True, min=1, not_empty=True)
386 386 private = v.StringBoolean(if_missing=False)
387 387 copy_permissions = v.StringBoolean(if_missing=False)
388 388 fork_parent_id = v.UnicodeString()
389 389 chained_validators = [v.ValidForkName(localizer, edit, old_data)]
390 390 return _RepoForkForm
391 391
392 392
393 393 def ApplicationSettingsForm(localizer):
394 394 _ = localizer
395 395
396 396 class _ApplicationSettingsForm(formencode.Schema):
397 397 allow_extra_fields = True
398 398 filter_extra_fields = False
399 399 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
400 400 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
401 401 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
402 402 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
403 403 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
404 404 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
405 405 rhodecode_create_personal_repo_group = v.StringBoolean(if_missing=False)
406 406 rhodecode_personal_repo_group_pattern = v.UnicodeString(strip=True, min=1, not_empty=False)
407 407 return _ApplicationSettingsForm
408 408
409 409
410 410 def ApplicationVisualisationForm(localizer):
411 411 from rhodecode.model.db import Repository
412 412 _ = localizer
413 413
414 414 class _ApplicationVisualisationForm(formencode.Schema):
415 415 allow_extra_fields = True
416 416 filter_extra_fields = False
417 417 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
418 418 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
419 419 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
420 420
421 421 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
422 422 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
423 423 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
424 424 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
425 425 rhodecode_show_version = v.StringBoolean(if_missing=False)
426 426 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
427 427 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
428 428 rhodecode_gravatar_url = v.UnicodeString(min=3)
429 429 rhodecode_clone_uri_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI)
430 430 rhodecode_clone_uri_id_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_ID)
431 431 rhodecode_clone_uri_ssh_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_SSH)
432 432 rhodecode_support_url = v.UnicodeString()
433 433 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
434 434 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
435 435 return _ApplicationVisualisationForm
436 436
437 437
438 438 class _BaseVcsSettingsForm(formencode.Schema):
439 439
440 440 allow_extra_fields = True
441 441 filter_extra_fields = False
442 442 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
443 443 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
444 444 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
445 445
446 446 # PR/Code-review
447 447 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
448 448 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
449 449
450 450 # hg
451 451 extensions_largefiles = v.StringBoolean(if_missing=False)
452 452 extensions_evolve = v.StringBoolean(if_missing=False)
453 453 phases_publish = v.StringBoolean(if_missing=False)
454 454
455 455 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
456 456 rhodecode_hg_close_branch_before_merging = v.StringBoolean(if_missing=False)
457 457
458 458 # git
459 459 vcs_git_lfs_enabled = v.StringBoolean(if_missing=False)
460 460 rhodecode_git_use_rebase_for_merging = v.StringBoolean(if_missing=False)
461 461 rhodecode_git_close_branch_before_merging = v.StringBoolean(if_missing=False)
462 462
463 463 # cache
464 464 rhodecode_diff_cache = v.StringBoolean(if_missing=False)
465 465
466 466
467 467 def ApplicationUiSettingsForm(localizer):
468 468 _ = localizer
469 469
470 470 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
471 web_push_ssl = v.StringBoolean(if_missing=False)
472 471 extensions_hggit = v.StringBoolean(if_missing=False)
473 472 new_svn_branch = v.ValidSvnPattern(localizer, section='vcs_svn_branch')
474 473 new_svn_tag = v.ValidSvnPattern(localizer, section='vcs_svn_tag')
475 474 return _ApplicationUiSettingsForm
476 475
477 476
478 477 def RepoVcsSettingsForm(localizer, repo_name):
479 478 _ = localizer
480 479
481 480 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
482 481 inherit_global_settings = v.StringBoolean(if_missing=False)
483 482 new_svn_branch = v.ValidSvnPattern(localizer,
484 483 section='vcs_svn_branch', repo_name=repo_name)
485 484 new_svn_tag = v.ValidSvnPattern(localizer,
486 485 section='vcs_svn_tag', repo_name=repo_name)
487 486 return _RepoVcsSettingsForm
488 487
489 488
490 489 def LabsSettingsForm(localizer):
491 490 _ = localizer
492 491
493 492 class _LabSettingsForm(formencode.Schema):
494 493 allow_extra_fields = True
495 494 filter_extra_fields = False
496 495 return _LabSettingsForm
497 496
498 497
499 498 def ApplicationPermissionsForm(
500 499 localizer, register_choices, password_reset_choices,
501 500 extern_activate_choices):
502 501 _ = localizer
503 502
504 503 class _DefaultPermissionsForm(formencode.Schema):
505 504 allow_extra_fields = True
506 505 filter_extra_fields = True
507 506
508 507 anonymous = v.StringBoolean(if_missing=False)
509 508 default_register = v.OneOf(register_choices)
510 509 default_register_message = v.UnicodeString()
511 510 default_password_reset = v.OneOf(password_reset_choices)
512 511 default_extern_activate = v.OneOf(extern_activate_choices)
513 512 return _DefaultPermissionsForm
514 513
515 514
516 515 def ObjectPermissionsForm(localizer, repo_perms_choices, group_perms_choices,
517 516 user_group_perms_choices):
518 517 _ = localizer
519 518
520 519 class _ObjectPermissionsForm(formencode.Schema):
521 520 allow_extra_fields = True
522 521 filter_extra_fields = True
523 522 overwrite_default_repo = v.StringBoolean(if_missing=False)
524 523 overwrite_default_group = v.StringBoolean(if_missing=False)
525 524 overwrite_default_user_group = v.StringBoolean(if_missing=False)
526 525
527 526 default_repo_perm = v.OneOf(repo_perms_choices)
528 527 default_group_perm = v.OneOf(group_perms_choices)
529 528 default_user_group_perm = v.OneOf(user_group_perms_choices)
530 529
531 530 return _ObjectPermissionsForm
532 531
533 532
534 533 def BranchPermissionsForm(localizer, branch_perms_choices):
535 534 _ = localizer
536 535
537 536 class _BranchPermissionsForm(formencode.Schema):
538 537 allow_extra_fields = True
539 538 filter_extra_fields = True
540 539 overwrite_default_branch = v.StringBoolean(if_missing=False)
541 540 default_branch_perm = v.OneOf(branch_perms_choices)
542 541
543 542 return _BranchPermissionsForm
544 543
545 544
546 545 def UserPermissionsForm(localizer, create_choices, create_on_write_choices,
547 546 repo_group_create_choices, user_group_create_choices,
548 547 fork_choices, inherit_default_permissions_choices):
549 548 _ = localizer
550 549
551 550 class _DefaultPermissionsForm(formencode.Schema):
552 551 allow_extra_fields = True
553 552 filter_extra_fields = True
554 553
555 554 anonymous = v.StringBoolean(if_missing=False)
556 555
557 556 default_repo_create = v.OneOf(create_choices)
558 557 default_repo_create_on_write = v.OneOf(create_on_write_choices)
559 558 default_user_group_create = v.OneOf(user_group_create_choices)
560 559 default_repo_group_create = v.OneOf(repo_group_create_choices)
561 560 default_fork_create = v.OneOf(fork_choices)
562 561 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
563 562 return _DefaultPermissionsForm
564 563
565 564
566 565 def UserIndividualPermissionsForm(localizer):
567 566 _ = localizer
568 567
569 568 class _DefaultPermissionsForm(formencode.Schema):
570 569 allow_extra_fields = True
571 570 filter_extra_fields = True
572 571
573 572 inherit_default_permissions = v.StringBoolean(if_missing=False)
574 573 return _DefaultPermissionsForm
575 574
576 575
577 576 def DefaultsForm(localizer, edit=False, old_data=None, supported_backends=BACKENDS.keys()):
578 577 _ = localizer
579 578 old_data = old_data or {}
580 579
581 580 class _DefaultsForm(formencode.Schema):
582 581 allow_extra_fields = True
583 582 filter_extra_fields = True
584 583 default_repo_type = v.OneOf(supported_backends)
585 584 default_repo_private = v.StringBoolean(if_missing=False)
586 585 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
587 586 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
588 587 default_repo_enable_locking = v.StringBoolean(if_missing=False)
589 588 return _DefaultsForm
590 589
591 590
592 591 def AuthSettingsForm(localizer):
593 592 _ = localizer
594 593
595 594 class _AuthSettingsForm(formencode.Schema):
596 595 allow_extra_fields = True
597 596 filter_extra_fields = True
598 597 auth_plugins = All(v.ValidAuthPlugins(localizer),
599 598 v.UniqueListFromString(localizer)(not_empty=True))
600 599 return _AuthSettingsForm
601 600
602 601
603 602 def UserExtraEmailForm(localizer):
604 603 _ = localizer
605 604
606 605 class _UserExtraEmailForm(formencode.Schema):
607 606 email = All(v.UniqSystemEmail(localizer), v.Email(not_empty=True))
608 607 return _UserExtraEmailForm
609 608
610 609
611 610 def UserExtraIpForm(localizer):
612 611 _ = localizer
613 612
614 613 class _UserExtraIpForm(formencode.Schema):
615 614 ip = v.ValidIp(localizer)(not_empty=True)
616 615 return _UserExtraIpForm
617 616
618 617
619 618 def PullRequestForm(localizer, repo_id):
620 619 _ = localizer
621 620
622 621 class ReviewerForm(formencode.Schema):
623 622 user_id = v.Int(not_empty=True)
624 623 reasons = All()
625 624 rules = All(v.UniqueList(localizer, convert=int)())
626 625 mandatory = v.StringBoolean()
627 626 role = v.String(if_missing='reviewer')
628 627
629 628 class ObserverForm(formencode.Schema):
630 629 user_id = v.Int(not_empty=True)
631 630 reasons = All()
632 631 rules = All(v.UniqueList(localizer, convert=int)())
633 632 mandatory = v.StringBoolean()
634 633 role = v.String(if_missing='observer')
635 634
636 635 class _PullRequestForm(formencode.Schema):
637 636 allow_extra_fields = True
638 637 filter_extra_fields = True
639 638
640 639 common_ancestor = v.UnicodeString(strip=True, required=True)
641 640 source_repo = v.UnicodeString(strip=True, required=True)
642 641 source_ref = v.UnicodeString(strip=True, required=True)
643 642 target_repo = v.UnicodeString(strip=True, required=True)
644 643 target_ref = v.UnicodeString(strip=True, required=True)
645 644 revisions = All(#v.NotReviewedRevisions(localizer, repo_id)(),
646 645 v.UniqueList(localizer)(not_empty=True))
647 646 review_members = formencode.ForEach(ReviewerForm())
648 647 observer_members = formencode.ForEach(ObserverForm())
649 648 pullrequest_title = v.UnicodeString(strip=True, required=True, min=1, max=255)
650 649 pullrequest_desc = v.UnicodeString(strip=True, required=False)
651 650 description_renderer = v.UnicodeString(strip=True, required=False)
652 651
653 652 return _PullRequestForm
654 653
655 654
656 655 def IssueTrackerPatternsForm(localizer):
657 656 _ = localizer
658 657
659 658 class _IssueTrackerPatternsForm(formencode.Schema):
660 659 allow_extra_fields = True
661 660 filter_extra_fields = False
662 661 chained_validators = [v.ValidPattern(localizer)]
663 662 return _IssueTrackerPatternsForm
@@ -1,893 +1,888 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import os
20 20 import re
21 21 import logging
22 22 import time
23 23 import functools
24 24 from collections import namedtuple
25 25
26 26 from pyramid.threadlocal import get_current_request
27 27
28 28 from rhodecode.lib import rc_cache
29 29 from rhodecode.lib.hash_utils import sha1_safe
30 30 from rhodecode.lib.html_filters import sanitize_html
31 31 from rhodecode.lib.utils2 import (
32 32 Optional, AttributeDict, safe_str, remove_prefix, str2bool)
33 33 from rhodecode.lib.vcs.backends import base
34 34 from rhodecode.lib.statsd_client import StatsdClient
35 35 from rhodecode.model import BaseModel
36 36 from rhodecode.model.db import (
37 37 RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi, RhodeCodeSetting)
38 38 from rhodecode.model.meta import Session
39 39
40 40
41 41 log = logging.getLogger(__name__)
42 42
43 43
44 44 UiSetting = namedtuple(
45 45 'UiSetting', ['section', 'key', 'value', 'active'])
46 46
47 47 SOCIAL_PLUGINS_LIST = ['github', 'bitbucket', 'twitter', 'google']
48 48
49 49
50 50 class SettingNotFound(Exception):
51 51 def __init__(self, setting_id):
52 52 msg = f'Setting `{setting_id}` is not found'
53 53 super().__init__(msg)
54 54
55 55
56 56 class SettingsModel(BaseModel):
57 57 BUILTIN_HOOKS = (
58 58 RhodeCodeUi.HOOK_REPO_SIZE, RhodeCodeUi.HOOK_PUSH,
59 59 RhodeCodeUi.HOOK_PRE_PUSH, RhodeCodeUi.HOOK_PRETX_PUSH,
60 60 RhodeCodeUi.HOOK_PULL, RhodeCodeUi.HOOK_PRE_PULL,
61 61 RhodeCodeUi.HOOK_PUSH_KEY,)
62 62 HOOKS_SECTION = 'hooks'
63 63
64 64 def __init__(self, sa=None, repo=None):
65 65 self.repo = repo
66 66 self.UiDbModel = RepoRhodeCodeUi if repo else RhodeCodeUi
67 67 self.SettingsDbModel = (
68 68 RepoRhodeCodeSetting if repo else RhodeCodeSetting)
69 69 super().__init__(sa)
70 70
71 71 def get_keyname(self, key_name, prefix='rhodecode_'):
72 72 return f'{prefix}{key_name}'
73 73
74 74 def get_ui_by_key(self, key):
75 75 q = self.UiDbModel.query()
76 76 q = q.filter(self.UiDbModel.ui_key == key)
77 77 q = self._filter_by_repo(RepoRhodeCodeUi, q)
78 78 return q.scalar()
79 79
80 80 def get_ui_by_section(self, section):
81 81 q = self.UiDbModel.query()
82 82 q = q.filter(self.UiDbModel.ui_section == section)
83 83 q = self._filter_by_repo(RepoRhodeCodeUi, q)
84 84 return q.all()
85 85
86 86 def get_ui_by_section_and_key(self, section, key):
87 87 q = self.UiDbModel.query()
88 88 q = q.filter(self.UiDbModel.ui_section == section)
89 89 q = q.filter(self.UiDbModel.ui_key == key)
90 90 q = self._filter_by_repo(RepoRhodeCodeUi, q)
91 91 return q.scalar()
92 92
93 93 def get_ui(self, section=None, key=None):
94 94 q = self.UiDbModel.query()
95 95 q = self._filter_by_repo(RepoRhodeCodeUi, q)
96 96
97 97 if section:
98 98 q = q.filter(self.UiDbModel.ui_section == section)
99 99 if key:
100 100 q = q.filter(self.UiDbModel.ui_key == key)
101 101
102 102 # TODO: mikhail: add caching
103 103 result = [
104 104 UiSetting(
105 105 section=safe_str(r.ui_section), key=safe_str(r.ui_key),
106 106 value=safe_str(r.ui_value), active=r.ui_active
107 107 )
108 108 for r in q.all()
109 109 ]
110 110 return result
111 111
112 112 def get_builtin_hooks(self):
113 113 q = self.UiDbModel.query()
114 114 q = q.filter(self.UiDbModel.ui_key.in_(self.BUILTIN_HOOKS))
115 115 return self._get_hooks(q)
116 116
117 117 def get_custom_hooks(self):
118 118 q = self.UiDbModel.query()
119 119 q = q.filter(~self.UiDbModel.ui_key.in_(self.BUILTIN_HOOKS))
120 120 return self._get_hooks(q)
121 121
122 122 def create_ui_section_value(self, section, val, key=None, active=True):
123 123 new_ui = self.UiDbModel()
124 124 new_ui.ui_section = section
125 125 new_ui.ui_value = val
126 126 new_ui.ui_active = active
127 127
128 128 repository_id = ''
129 129 if self.repo:
130 130 repo = self._get_repo(self.repo)
131 131 repository_id = repo.repo_id
132 132 new_ui.repository_id = repository_id
133 133
134 134 if not key:
135 135 # keys are unique so they need appended info
136 136 if self.repo:
137 137 key = sha1_safe(f'{section}{val}{repository_id}')
138 138 else:
139 139 key = sha1_safe(f'{section}{val}')
140 140
141 141 new_ui.ui_key = key
142 142
143 143 Session().add(new_ui)
144 144 return new_ui
145 145
146 146 def create_or_update_hook(self, key, value):
147 147 ui = (
148 148 self.get_ui_by_section_and_key(self.HOOKS_SECTION, key) or
149 149 self.UiDbModel())
150 150 ui.ui_section = self.HOOKS_SECTION
151 151 ui.ui_active = True
152 152 ui.ui_key = key
153 153 ui.ui_value = value
154 154
155 155 if self.repo:
156 156 repo = self._get_repo(self.repo)
157 157 repository_id = repo.repo_id
158 158 ui.repository_id = repository_id
159 159
160 160 Session().add(ui)
161 161 return ui
162 162
163 163 def delete_ui(self, id_):
164 164 ui = self.UiDbModel.get(id_)
165 165 if not ui:
166 166 raise SettingNotFound(id_)
167 167 Session().delete(ui)
168 168
169 169 def get_setting_by_name(self, name):
170 170 q = self._get_settings_query()
171 171 q = q.filter(self.SettingsDbModel.app_settings_name == name)
172 172 return q.scalar()
173 173
174 174 def create_or_update_setting(
175 175 self, name, val: Optional | str = Optional(''), type_: Optional | str = Optional('unicode')):
176 176 """
177 177 Creates or updates RhodeCode setting. If updates are triggered, it will
178 178 only update parameters that are explicitly set Optional instance will
179 179 be skipped
180 180
181 181 :param name:
182 182 :param val:
183 183 :param type_:
184 184 :return:
185 185 """
186 186
187 187 res = self.get_setting_by_name(name)
188 188 repo = self._get_repo(self.repo) if self.repo else None
189 189
190 190 if not res:
191 191 val = Optional.extract(val)
192 192 type_ = Optional.extract(type_)
193 193
194 194 args = (
195 195 (repo.repo_id, name, val, type_)
196 196 if repo else (name, val, type_))
197 197 res = self.SettingsDbModel(*args)
198 198
199 199 else:
200 200 if self.repo:
201 201 res.repository_id = repo.repo_id
202 202
203 203 res.app_settings_name = name
204 204 if not isinstance(type_, Optional):
205 205 # update if set
206 206 res.app_settings_type = type_
207 207 if not isinstance(val, Optional):
208 208 # update if set
209 209 res.app_settings_value = val
210 210
211 211 Session().add(res)
212 212 return res
213 213
214 214 def get_cache_region(self):
215 215 repo = self._get_repo(self.repo) if self.repo else None
216 216 cache_key = f"repo.v1.{repo.repo_id}" if repo else "repo.v1.ALL"
217 217 cache_namespace_uid = f'cache_settings.{cache_key}'
218 218 region = rc_cache.get_or_create_region('cache_general', cache_namespace_uid)
219 219 return region, cache_namespace_uid
220 220
221 221 def invalidate_settings_cache(self, hard=False):
222 222 region, namespace_key = self.get_cache_region()
223 223 log.debug('Invalidation cache [%s] region %s for cache_key: %s',
224 224 'invalidate_settings_cache', region, namespace_key)
225 225
226 226 # we use hard cleanup if invalidation is sent
227 227 rc_cache.clear_cache_namespace(region, namespace_key, method=rc_cache.CLEAR_DELETE)
228 228
229 229 def get_cache_call_method(self, cache=True):
230 230 region, cache_key = self.get_cache_region()
231 231
232 232 @region.conditional_cache_on_arguments(condition=cache)
233 233 def _get_all_settings(name, key):
234 234 q = self._get_settings_query()
235 235 if not q:
236 236 raise Exception('Could not get application settings !')
237 237
238 238 settings = {
239 239 self.get_keyname(res.app_settings_name): res.app_settings_value
240 240 for res in q
241 241 }
242 242 return settings
243 243 return _get_all_settings
244 244
245 245 def get_all_settings(self, cache=False, from_request=True):
246 246 # defines if we use GLOBAL, or PER_REPO
247 247 repo = self._get_repo(self.repo) if self.repo else None
248 248
249 249 # initially try the request context; this is the fastest
250 250 # we only fetch global config, NOT for repo-specific
251 251 if from_request and not repo:
252 252 request = get_current_request()
253 253
254 254 if request and hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
255 255 rc_config = request.call_context.rc_config
256 256 if rc_config:
257 257 return rc_config
258 258
259 259 _region, cache_key = self.get_cache_region()
260 260 _get_all_settings = self.get_cache_call_method(cache=cache)
261 261
262 262 start = time.time()
263 263 result = _get_all_settings('rhodecode_settings', cache_key)
264 264 compute_time = time.time() - start
265 265 log.debug('cached method:%s took %.4fs', _get_all_settings.__name__, compute_time)
266 266
267 267 statsd = StatsdClient.statsd
268 268 if statsd:
269 269 elapsed_time_ms = round(1000.0 * compute_time) # use ms only
270 270 statsd.timing("rhodecode_settings_timing.histogram", elapsed_time_ms,
271 271 use_decimals=False)
272 272
273 273 log.debug('Fetching app settings for key: %s took: %.4fs: cache: %s', cache_key, compute_time, cache)
274 274
275 275 return result
276 276
277 277 def get_auth_settings(self):
278 278 q = self._get_settings_query()
279 279 q = q.filter(
280 280 self.SettingsDbModel.app_settings_name.startswith('auth_'))
281 281 rows = q.all()
282 282 auth_settings = {
283 283 row.app_settings_name: row.app_settings_value for row in rows}
284 284 return auth_settings
285 285
286 286 def get_auth_plugins(self):
287 287 auth_plugins = self.get_setting_by_name("auth_plugins")
288 288 return auth_plugins.app_settings_value
289 289
290 290 def get_default_repo_settings(self, strip_prefix=False):
291 291 q = self._get_settings_query()
292 292 q = q.filter(
293 293 self.SettingsDbModel.app_settings_name.startswith('default_'))
294 294 rows = q.all()
295 295
296 296 result = {}
297 297 for row in rows:
298 298 key = row.app_settings_name
299 299 if strip_prefix:
300 300 key = remove_prefix(key, prefix='default_')
301 301 result.update({key: row.app_settings_value})
302 302 return result
303 303
304 304 def get_repo(self):
305 305 repo = self._get_repo(self.repo)
306 306 if not repo:
307 307 raise Exception(
308 308 f'Repository `{self.repo}` cannot be found inside the database')
309 309 return repo
310 310
311 311 def _filter_by_repo(self, model, query):
312 312 if self.repo:
313 313 repo = self.get_repo()
314 314 query = query.filter(model.repository_id == repo.repo_id)
315 315 return query
316 316
317 317 def _get_hooks(self, query):
318 318 query = query.filter(self.UiDbModel.ui_section == self.HOOKS_SECTION)
319 319 query = self._filter_by_repo(RepoRhodeCodeUi, query)
320 320 return query.all()
321 321
322 322 def _get_settings_query(self):
323 323 q = self.SettingsDbModel.query()
324 324 return self._filter_by_repo(RepoRhodeCodeSetting, q)
325 325
326 326 def list_enabled_social_plugins(self, settings):
327 327 enabled = []
328 328 for plug in SOCIAL_PLUGINS_LIST:
329 329 if str2bool(settings.get(f'rhodecode_auth_{plug}_enabled')):
330 330 enabled.append(plug)
331 331 return enabled
332 332
333 333
334 334 def assert_repo_settings(func):
335 335 @functools.wraps(func)
336 336 def _wrapper(self, *args, **kwargs):
337 337 if not self.repo_settings:
338 338 raise Exception('Repository is not specified')
339 339 return func(self, *args, **kwargs)
340 340 return _wrapper
341 341
342 342
343 343 class IssueTrackerSettingsModel(object):
344 344 INHERIT_SETTINGS = 'inherit_issue_tracker_settings'
345 345 SETTINGS_PREFIX = 'issuetracker_'
346 346
347 347 def __init__(self, sa=None, repo=None):
348 348 self.global_settings = SettingsModel(sa=sa)
349 349 self.repo_settings = SettingsModel(sa=sa, repo=repo) if repo else None
350 350
351 351 @property
352 352 def inherit_global_settings(self):
353 353 if not self.repo_settings:
354 354 return True
355 355 setting = self.repo_settings.get_setting_by_name(self.INHERIT_SETTINGS)
356 356 return setting.app_settings_value if setting else True
357 357
358 358 @inherit_global_settings.setter
359 359 def inherit_global_settings(self, value):
360 360 if self.repo_settings:
361 361 settings = self.repo_settings.create_or_update_setting(
362 362 self.INHERIT_SETTINGS, value, type_='bool')
363 363 Session().add(settings)
364 364
365 365 def _get_keyname(self, key, uid, prefix='rhodecode_'):
366 366 return f'{prefix}{self.SETTINGS_PREFIX}{key}_{uid}'
367 367
368 368 def _make_dict_for_settings(self, qs):
369 369 prefix_match = self._get_keyname('pat', '',)
370 370
371 371 issuetracker_entries = {}
372 372 # create keys
373 373 for k, v in qs.items():
374 374 if k.startswith(prefix_match):
375 375 uid = k[len(prefix_match):]
376 376 issuetracker_entries[uid] = None
377 377
378 378 def url_cleaner(input_str):
379 379 input_str = input_str.replace('"', '').replace("'", '')
380 380 input_str = sanitize_html(input_str, strip=True)
381 381 return input_str
382 382
383 383 # populate
384 384 for uid in issuetracker_entries:
385 385 url_data = qs.get(self._get_keyname('url', uid))
386 386
387 387 pat = qs.get(self._get_keyname('pat', uid))
388 388 try:
389 389 pat_compiled = re.compile(r'%s' % pat)
390 390 except re.error:
391 391 pat_compiled = None
392 392
393 393 issuetracker_entries[uid] = AttributeDict({
394 394 'pat': pat,
395 395 'pat_compiled': pat_compiled,
396 396 'url': url_cleaner(
397 397 qs.get(self._get_keyname('url', uid)) or ''),
398 398 'pref': sanitize_html(
399 399 qs.get(self._get_keyname('pref', uid)) or ''),
400 400 'desc': qs.get(
401 401 self._get_keyname('desc', uid)),
402 402 })
403 403
404 404 return issuetracker_entries
405 405
406 406 def get_global_settings(self, cache=False):
407 407 """
408 408 Returns list of global issue tracker settings
409 409 """
410 410 defaults = self.global_settings.get_all_settings(cache=cache)
411 411 settings = self._make_dict_for_settings(defaults)
412 412 return settings
413 413
414 414 def get_repo_settings(self, cache=False):
415 415 """
416 416 Returns list of issue tracker settings per repository
417 417 """
418 418 if not self.repo_settings:
419 419 raise Exception('Repository is not specified')
420 420 all_settings = self.repo_settings.get_all_settings(cache=cache)
421 421 settings = self._make_dict_for_settings(all_settings)
422 422 return settings
423 423
424 424 def get_settings(self, cache=False):
425 425 if self.inherit_global_settings:
426 426 return self.get_global_settings(cache=cache)
427 427 else:
428 428 return self.get_repo_settings(cache=cache)
429 429
430 430 def delete_entries(self, uid):
431 431 if self.repo_settings:
432 432 all_patterns = self.get_repo_settings()
433 433 settings_model = self.repo_settings
434 434 else:
435 435 all_patterns = self.get_global_settings()
436 436 settings_model = self.global_settings
437 437 entries = all_patterns.get(uid, [])
438 438
439 439 for del_key in entries:
440 440 setting_name = self._get_keyname(del_key, uid, prefix='')
441 441 entry = settings_model.get_setting_by_name(setting_name)
442 442 if entry:
443 443 Session().delete(entry)
444 444
445 445 Session().commit()
446 446
447 447 def create_or_update_setting(
448 448 self, name, val=Optional(''), type_=Optional('unicode')):
449 449 if self.repo_settings:
450 450 setting = self.repo_settings.create_or_update_setting(
451 451 name, val, type_)
452 452 else:
453 453 setting = self.global_settings.create_or_update_setting(
454 454 name, val, type_)
455 455 return setting
456 456
457 457
458 458 class VcsSettingsModel(object):
459 459
460 460 INHERIT_SETTINGS = 'inherit_vcs_settings'
461 461 GENERAL_SETTINGS = (
462 462 'use_outdated_comments',
463 463 'pr_merge_enabled',
464 464 'hg_use_rebase_for_merging',
465 465 'hg_close_branch_before_merging',
466 466 'git_use_rebase_for_merging',
467 467 'git_close_branch_before_merging',
468 468 'diff_cache',
469 469 )
470 470
471 471 HOOKS_SETTINGS = (
472 472 ('hooks', 'changegroup.repo_size'),
473 473 ('hooks', 'changegroup.push_logger'),
474 474 ('hooks', 'outgoing.pull_logger'),
475 475 )
476 476 HG_SETTINGS = (
477 477 ('extensions', 'largefiles'),
478 478 ('phases', 'publish'),
479 479 ('extensions', 'evolve'),
480 480 ('extensions', 'topic'),
481 481 ('experimental', 'evolution'),
482 482 ('experimental', 'evolution.exchange'),
483 483 )
484 484 GIT_SETTINGS = (
485 485 ('vcs_git_lfs', 'enabled'),
486 486 )
487 487 GLOBAL_HG_SETTINGS = (
488 488 ('extensions', 'largefiles'),
489 489 ('phases', 'publish'),
490 490 ('extensions', 'evolve'),
491 491 ('extensions', 'topic'),
492 492 ('experimental', 'evolution'),
493 493 ('experimental', 'evolution.exchange'),
494 494 )
495 495
496 496 GLOBAL_GIT_SETTINGS = (
497 497 ('vcs_git_lfs', 'enabled'),
498 498 )
499 499
500 500 SVN_BRANCH_SECTION = 'vcs_svn_branch'
501 501 SVN_TAG_SECTION = 'vcs_svn_tag'
502 SSL_SETTING = ('web', 'push_ssl')
503 502 PATH_SETTING = ('paths', '/')
504 503
505 504 def __init__(self, sa=None, repo=None):
506 505 self.global_settings = SettingsModel(sa=sa)
507 506 self.repo_settings = SettingsModel(sa=sa, repo=repo) if repo else None
508 507 self._ui_settings = (
509 508 self.HG_SETTINGS + self.GIT_SETTINGS + self.HOOKS_SETTINGS)
510 509 self._svn_sections = (self.SVN_BRANCH_SECTION, self.SVN_TAG_SECTION)
511 510
512 511 @property
513 512 @assert_repo_settings
514 513 def inherit_global_settings(self):
515 514 setting = self.repo_settings.get_setting_by_name(self.INHERIT_SETTINGS)
516 515 return setting.app_settings_value if setting else True
517 516
518 517 @inherit_global_settings.setter
519 518 @assert_repo_settings
520 519 def inherit_global_settings(self, value):
521 520 self.repo_settings.create_or_update_setting(
522 521 self.INHERIT_SETTINGS, value, type_='bool')
523 522
524 523 def get_keyname(self, key_name, prefix='rhodecode_'):
525 524 return f'{prefix}{key_name}'
526 525
527 526 def get_global_svn_branch_patterns(self):
528 527 return self.global_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
529 528
530 529 @assert_repo_settings
531 530 def get_repo_svn_branch_patterns(self):
532 531 return self.repo_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
533 532
534 533 def get_global_svn_tag_patterns(self):
535 534 return self.global_settings.get_ui_by_section(self.SVN_TAG_SECTION)
536 535
537 536 @assert_repo_settings
538 537 def get_repo_svn_tag_patterns(self):
539 538 return self.repo_settings.get_ui_by_section(self.SVN_TAG_SECTION)
540 539
541 540 def get_global_settings(self):
542 541 return self._collect_all_settings(global_=True)
543 542
544 543 @assert_repo_settings
545 544 def get_repo_settings(self):
546 545 return self._collect_all_settings(global_=False)
547 546
548 547 @assert_repo_settings
549 548 def get_repo_settings_inherited(self):
550 549 global_settings = self.get_global_settings()
551 550 global_settings.update(self.get_repo_settings())
552 551 return global_settings
553 552
554 553 @assert_repo_settings
555 554 def create_or_update_repo_settings(
556 555 self, data, inherit_global_settings=False):
557 556 from rhodecode.model.scm import ScmModel
558 557
559 558 self.inherit_global_settings = inherit_global_settings
560 559
561 560 repo = self.repo_settings.get_repo()
562 561 if not inherit_global_settings:
563 562 if repo.repo_type == 'svn':
564 563 self.create_repo_svn_settings(data)
565 564 else:
566 565 self.create_or_update_repo_hook_settings(data)
567 566 self.create_or_update_repo_pr_settings(data)
568 567
569 568 if repo.repo_type == 'hg':
570 569 self.create_or_update_repo_hg_settings(data)
571 570
572 571 if repo.repo_type == 'git':
573 572 self.create_or_update_repo_git_settings(data)
574 573
575 574 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
576 575
577 576 @assert_repo_settings
578 577 def create_or_update_repo_hook_settings(self, data):
579 578 for section, key in self.HOOKS_SETTINGS:
580 579 data_key = self._get_form_ui_key(section, key)
581 580 if data_key not in data:
582 581 raise ValueError(
583 582 f'The given data does not contain {data_key} key')
584 583
585 584 active = data.get(data_key)
586 585 repo_setting = self.repo_settings.get_ui_by_section_and_key(
587 586 section, key)
588 587 if not repo_setting:
589 588 global_setting = self.global_settings.\
590 589 get_ui_by_section_and_key(section, key)
591 590 self.repo_settings.create_ui_section_value(
592 591 section, global_setting.ui_value, key=key, active=active)
593 592 else:
594 593 repo_setting.ui_active = active
595 594 Session().add(repo_setting)
596 595
597 596 def update_global_hook_settings(self, data):
598 597 for section, key in self.HOOKS_SETTINGS:
599 598 data_key = self._get_form_ui_key(section, key)
600 599 if data_key not in data:
601 600 raise ValueError(
602 601 f'The given data does not contain {data_key} key')
603 602 active = data.get(data_key)
604 603 repo_setting = self.global_settings.get_ui_by_section_and_key(
605 604 section, key)
606 605 repo_setting.ui_active = active
607 606 Session().add(repo_setting)
608 607
609 608 @assert_repo_settings
610 609 def create_or_update_repo_pr_settings(self, data):
611 610 return self._create_or_update_general_settings(
612 611 self.repo_settings, data)
613 612
614 613 def create_or_update_global_pr_settings(self, data):
615 614 return self._create_or_update_general_settings(
616 615 self.global_settings, data)
617 616
618 617 @assert_repo_settings
619 618 def create_repo_svn_settings(self, data):
620 619 return self._create_svn_settings(self.repo_settings, data)
621 620
622 621 def _set_evolution(self, settings, is_enabled):
623 622 if is_enabled:
624 623 # if evolve is active set evolution=all
625 624
626 625 self._create_or_update_ui(
627 626 settings, *('experimental', 'evolution'), value='all',
628 627 active=True)
629 628 self._create_or_update_ui(
630 629 settings, *('experimental', 'evolution.exchange'), value='yes',
631 630 active=True)
632 631 # if evolve is active set topics server support
633 632 self._create_or_update_ui(
634 633 settings, *('extensions', 'topic'), value='',
635 634 active=True)
636 635
637 636 else:
638 637 self._create_or_update_ui(
639 638 settings, *('experimental', 'evolution'), value='',
640 639 active=False)
641 640 self._create_or_update_ui(
642 641 settings, *('experimental', 'evolution.exchange'), value='no',
643 642 active=False)
644 643 self._create_or_update_ui(
645 644 settings, *('extensions', 'topic'), value='',
646 645 active=False)
647 646
648 647 @assert_repo_settings
649 648 def create_or_update_repo_hg_settings(self, data):
650 649 largefiles, phases, evolve = \
651 650 self.HG_SETTINGS[:3]
652 651 largefiles_key, phases_key, evolve_key = \
653 652 self._get_settings_keys(self.HG_SETTINGS[:3], data)
654 653
655 654 self._create_or_update_ui(
656 655 self.repo_settings, *largefiles, value='',
657 656 active=data[largefiles_key])
658 657 self._create_or_update_ui(
659 658 self.repo_settings, *evolve, value='',
660 659 active=data[evolve_key])
661 660 self._set_evolution(self.repo_settings, is_enabled=data[evolve_key])
662 661
663 662 self._create_or_update_ui(
664 663 self.repo_settings, *phases, value=safe_str(data[phases_key]))
665 664
666 665 def create_or_update_global_hg_settings(self, data):
667 666 opts_len = 3
668 667 largefiles, phases, evolve \
669 668 = self.GLOBAL_HG_SETTINGS[:opts_len]
670 669 largefiles_key, phases_key, evolve_key \
671 670 = self._get_settings_keys(self.GLOBAL_HG_SETTINGS[:opts_len], data)
672 671
673 672 self._create_or_update_ui(
674 673 self.global_settings, *largefiles, value='',
675 674 active=data[largefiles_key])
676 675 self._create_or_update_ui(
677 676 self.global_settings, *phases, value=safe_str(data[phases_key]))
678 677 self._create_or_update_ui(
679 678 self.global_settings, *evolve, value='',
680 679 active=data[evolve_key])
681 680 self._set_evolution(self.global_settings, is_enabled=data[evolve_key])
682 681
683 682 def create_or_update_repo_git_settings(self, data):
684 683 # NOTE(marcink): # comma makes unpack work properly
685 684 lfs_enabled, \
686 685 = self.GIT_SETTINGS
687 686
688 687 lfs_enabled_key, \
689 688 = self._get_settings_keys(self.GIT_SETTINGS, data)
690 689
691 690 self._create_or_update_ui(
692 691 self.repo_settings, *lfs_enabled, value=data[lfs_enabled_key],
693 692 active=data[lfs_enabled_key])
694 693
695 694 def create_or_update_global_git_settings(self, data):
696 695 lfs_enabled = self.GLOBAL_GIT_SETTINGS[0]
697 696 lfs_enabled_key = self._get_settings_keys(self.GLOBAL_GIT_SETTINGS, data)[0]
698 697
699 698 self._create_or_update_ui(
700 699 self.global_settings, *lfs_enabled, value=data[lfs_enabled_key],
701 700 active=data[lfs_enabled_key])
702 701
703 702 def create_or_update_global_svn_settings(self, data):
704 703 # branch/tags patterns
705 704 self._create_svn_settings(self.global_settings, data)
706 705
707 def update_global_ssl_setting(self, value):
708 self._create_or_update_ui(
709 self.global_settings, *self.SSL_SETTING, value=value)
710
711 706 @assert_repo_settings
712 707 def delete_repo_svn_pattern(self, id_):
713 708 ui = self.repo_settings.UiDbModel.get(id_)
714 709 if ui and ui.repository.repo_name == self.repo_settings.repo:
715 710 # only delete if it's the same repo as initialized settings
716 711 self.repo_settings.delete_ui(id_)
717 712 else:
718 713 # raise error as if we wouldn't find this option
719 714 self.repo_settings.delete_ui(-1)
720 715
721 716 def delete_global_svn_pattern(self, id_):
722 717 self.global_settings.delete_ui(id_)
723 718
724 719 @assert_repo_settings
725 720 def get_repo_ui_settings(self, section=None, key=None):
726 721 global_uis = self.global_settings.get_ui(section, key)
727 722 repo_uis = self.repo_settings.get_ui(section, key)
728 723
729 724 filtered_repo_uis = self._filter_ui_settings(repo_uis)
730 725 filtered_repo_uis_keys = [
731 726 (s.section, s.key) for s in filtered_repo_uis]
732 727
733 728 def _is_global_ui_filtered(ui):
734 729 return (
735 730 (ui.section, ui.key) in filtered_repo_uis_keys
736 731 or ui.section in self._svn_sections)
737 732
738 733 filtered_global_uis = [
739 734 ui for ui in global_uis if not _is_global_ui_filtered(ui)]
740 735
741 736 return filtered_global_uis + filtered_repo_uis
742 737
743 738 def get_global_ui_settings(self, section=None, key=None):
744 739 return self.global_settings.get_ui(section, key)
745 740
746 741 def get_ui_settings_as_config_obj(self, section=None, key=None):
747 742 config = base.Config()
748 743
749 744 ui_settings = self.get_ui_settings(section=section, key=key)
750 745
751 746 for entry in ui_settings:
752 747 config.set(entry.section, entry.key, entry.value)
753 748
754 749 return config
755 750
756 751 def get_ui_settings(self, section=None, key=None):
757 752 if not self.repo_settings or self.inherit_global_settings:
758 753 return self.get_global_ui_settings(section, key)
759 754 else:
760 755 return self.get_repo_ui_settings(section, key)
761 756
762 757 def get_svn_patterns(self, section=None):
763 758 if not self.repo_settings:
764 759 return self.get_global_ui_settings(section)
765 760 else:
766 761 return self.get_repo_ui_settings(section)
767 762
768 763 @assert_repo_settings
769 764 def get_repo_general_settings(self):
770 765 global_settings = self.global_settings.get_all_settings()
771 766 repo_settings = self.repo_settings.get_all_settings()
772 767 filtered_repo_settings = self._filter_general_settings(repo_settings)
773 768 global_settings.update(filtered_repo_settings)
774 769 return global_settings
775 770
776 771 def get_global_general_settings(self):
777 772 return self.global_settings.get_all_settings()
778 773
779 774 def get_general_settings(self):
780 775 if not self.repo_settings or self.inherit_global_settings:
781 776 return self.get_global_general_settings()
782 777 else:
783 778 return self.get_repo_general_settings()
784 779
785 780 def _filter_ui_settings(self, settings):
786 781 filtered_settings = [
787 782 s for s in settings if self._should_keep_setting(s)]
788 783 return filtered_settings
789 784
790 785 def _should_keep_setting(self, setting):
791 786 keep = (
792 787 (setting.section, setting.key) in self._ui_settings or
793 788 setting.section in self._svn_sections)
794 789 return keep
795 790
796 791 def _filter_general_settings(self, settings):
797 792 keys = [self.get_keyname(key) for key in self.GENERAL_SETTINGS]
798 793 return {
799 794 k: settings[k]
800 795 for k in settings if k in keys}
801 796
802 797 def _collect_all_settings(self, global_=False):
803 798 settings = self.global_settings if global_ else self.repo_settings
804 799 result = {}
805 800
806 801 for section, key in self._ui_settings:
807 802 ui = settings.get_ui_by_section_and_key(section, key)
808 803 result_key = self._get_form_ui_key(section, key)
809 804
810 805 if ui:
811 806 if section in ('hooks', 'extensions'):
812 807 result[result_key] = ui.ui_active
813 808 elif result_key in ['vcs_git_lfs_enabled']:
814 809 result[result_key] = ui.ui_active
815 810 else:
816 811 result[result_key] = ui.ui_value
817 812
818 813 for name in self.GENERAL_SETTINGS:
819 814 setting = settings.get_setting_by_name(name)
820 815 if setting:
821 816 result_key = self.get_keyname(name)
822 817 result[result_key] = setting.app_settings_value
823 818
824 819 return result
825 820
826 821 def _get_form_ui_key(self, section, key):
827 822 return '{section}_{key}'.format(
828 823 section=section, key=key.replace('.', '_'))
829 824
830 825 def _create_or_update_ui(
831 826 self, settings, section, key, value=None, active=None):
832 827 ui = settings.get_ui_by_section_and_key(section, key)
833 828 if not ui:
834 829 active = True if active is None else active
835 830 settings.create_ui_section_value(
836 831 section, value, key=key, active=active)
837 832 else:
838 833 if active is not None:
839 834 ui.ui_active = active
840 835 if value is not None:
841 836 ui.ui_value = value
842 837 Session().add(ui)
843 838
844 839 def _create_svn_settings(self, settings, data):
845 840 svn_settings = {
846 841 'new_svn_branch': self.SVN_BRANCH_SECTION,
847 842 'new_svn_tag': self.SVN_TAG_SECTION
848 843 }
849 844 for key in svn_settings:
850 845 if data.get(key):
851 846 settings.create_ui_section_value(svn_settings[key], data[key])
852 847
853 848 def _create_or_update_general_settings(self, settings, data):
854 849 for name in self.GENERAL_SETTINGS:
855 850 data_key = self.get_keyname(name)
856 851 if data_key not in data:
857 852 raise ValueError(
858 853 f'The given data does not contain {data_key} key')
859 854 setting = settings.create_or_update_setting(
860 855 name, data[data_key], 'bool')
861 856 Session().add(setting)
862 857
863 858 def _get_settings_keys(self, settings, data):
864 859 data_keys = [self._get_form_ui_key(*s) for s in settings]
865 860 for data_key in data_keys:
866 861 if data_key not in data:
867 862 raise ValueError(
868 863 f'The given data does not contain {data_key} key')
869 864 return data_keys
870 865
871 866 def create_largeobjects_dirs_if_needed(self, repo_store_path):
872 867 """
873 868 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
874 869 does a repository scan if enabled in the settings.
875 870 """
876 871
877 872 from rhodecode.lib.vcs.backends.hg import largefiles_store
878 873 from rhodecode.lib.vcs.backends.git import lfs_store
879 874
880 875 paths = [
881 876 largefiles_store(repo_store_path),
882 877 lfs_store(repo_store_path)]
883 878
884 879 for path in paths:
885 880 if os.path.isdir(path):
886 881 continue
887 882 if os.path.isfile(path):
888 883 continue
889 884 # not a file nor dir, we try to create it
890 885 try:
891 886 os.makedirs(path)
892 887 except Exception:
893 888 log.warning('Failed to create largefiles dir:%s', path)
@@ -1,399 +1,414 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18 import io
19 19 import shlex
20 20
21 21 import math
22 22 import re
23 23 import os
24 24 import datetime
25 25 import logging
26 26 import queue
27 27 import subprocess
28 28
29 29
30 30 from dateutil.parser import parse
31 31 from pyramid.interfaces import IRoutesMapper
32 32 from pyramid.settings import asbool
33 33 from pyramid.path import AssetResolver
34 34 from threading import Thread
35 35
36 36 from rhodecode.config.jsroutes import generate_jsroutes_content
37 37 from rhodecode.lib.base import get_auth_user
38 38 from rhodecode.lib.celerylib.loader import set_celery_conf
39 39
40 40 import rhodecode
41 41
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 def add_renderer_globals(event):
47 47 from rhodecode.lib import helpers
48 48
49 49 # TODO: When executed in pyramid view context the request is not available
50 50 # in the event. Find a better solution to get the request.
51 51 from pyramid.threadlocal import get_current_request
52 52 request = event['request'] or get_current_request()
53 53
54 54 # Add Pyramid translation as '_' to context
55 55 event['_'] = request.translate
56 56 event['_ungettext'] = request.plularize
57 57 event['h'] = helpers
58 58
59 59
60 60 def set_user_lang(event):
61 61 request = event.request
62 62 cur_user = getattr(request, 'user', None)
63 63
64 64 if cur_user:
65 65 user_lang = cur_user.get_instance().user_data.get('language')
66 66 if user_lang:
67 67 log.debug('lang: setting current user:%s language to: %s', cur_user, user_lang)
68 68 event.request._LOCALE_ = user_lang
69 69
70 70
71 71 def update_celery_conf(event):
72 72 log.debug('Setting celery config from new request')
73 73 set_celery_conf(request=event.request, registry=event.request.registry)
74 74
75 75
76 76 def add_request_user_context(event):
77 77 """
78 78 Adds auth user into request context
79 79 """
80 80
81 81 request = event.request
82 82 # access req_id as soon as possible
83 83 req_id = request.req_id
84 84
85 85 if hasattr(request, 'vcs_call'):
86 86 # skip vcs calls
87 87 return
88 88
89 89 if hasattr(request, 'rpc_method'):
90 90 # skip api calls
91 91 return
92 92
93 93 auth_user, auth_token = get_auth_user(request)
94 94 request.user = auth_user
95 95 request.user_auth_token = auth_token
96 96 request.environ['rc_auth_user'] = auth_user
97 97 request.environ['rc_auth_user_id'] = str(auth_user.user_id)
98 98 request.environ['rc_req_id'] = req_id
99 99
100 100
101 101 def reset_log_bucket(event):
102 102 """
103 103 reset the log bucket on new request
104 104 """
105 105 request = event.request
106 106 request.req_id_records_init()
107 107
108 108
109 109 def scan_repositories_if_enabled(event):
110 110 """
111 111 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
112 112 does a repository scan if enabled in the settings.
113 113 """
114 114 settings = event.app.registry.settings
115 115 vcs_server_enabled = settings['vcs.server.enable']
116 116 import_on_startup = settings['startup.import_repos']
117 117 if vcs_server_enabled and import_on_startup:
118 118 from rhodecode.model.scm import ScmModel
119 119 from rhodecode.lib.utils import repo2db_mapper
120 120 scm = ScmModel()
121 121 repositories = scm.repo_scan(scm.repos_path)
122 122 repo2db_mapper(repositories, remove_obsolete=False)
123 123
124 124
125 125 def write_metadata_if_needed(event):
126 126 """
127 127 Writes upgrade metadata
128 128 """
129 129 import rhodecode
130 130 from rhodecode.lib import system_info
131 131 from rhodecode.lib import ext_json
132 132
133 133 fname = '.rcmetadata.json'
134 134 ini_loc = os.path.dirname(rhodecode.CONFIG.get('__file__'))
135 135 metadata_destination = os.path.join(ini_loc, fname)
136 136
137 137 def get_update_age():
138 138 now = datetime.datetime.utcnow()
139 139
140 140 with open(metadata_destination, 'rb') as f:
141 141 data = ext_json.json.loads(f.read())
142 142 if 'created_on' in data:
143 143 update_date = parse(data['created_on'])
144 144 diff = now - update_date
145 145 return diff.total_seconds() / 60.0
146 146
147 147 return 0
148 148
149 149 def write():
150 150 configuration = system_info.SysInfo(
151 151 system_info.rhodecode_config)()['value']
152 152 license_token = configuration['config']['license_token']
153 153
154 154 setup = dict(
155 155 workers=configuration['config']['server:main'].get(
156 156 'workers', '?'),
157 157 worker_type=configuration['config']['server:main'].get(
158 158 'worker_class', 'sync'),
159 159 )
160 160 dbinfo = system_info.SysInfo(system_info.database_info)()['value']
161 161 del dbinfo['url']
162 162
163 163 metadata = dict(
164 164 desc='upgrade metadata info',
165 165 license_token=license_token,
166 166 created_on=datetime.datetime.utcnow().isoformat(),
167 167 usage=system_info.SysInfo(system_info.usage_info)()['value'],
168 168 platform=system_info.SysInfo(system_info.platform_type)()['value'],
169 169 database=dbinfo,
170 170 cpu=system_info.SysInfo(system_info.cpu)()['value'],
171 171 memory=system_info.SysInfo(system_info.memory)()['value'],
172 172 setup=setup
173 173 )
174 174
175 175 with open(metadata_destination, 'wb') as f:
176 176 f.write(ext_json.json.dumps(metadata))
177 177
178 178 settings = event.app.registry.settings
179 179 if settings.get('metadata.skip'):
180 180 return
181 181
182 182 # only write this every 24h, workers restart caused unwanted delays
183 183 try:
184 184 age_in_min = get_update_age()
185 185 except Exception:
186 186 age_in_min = 0
187 187
188 188 if age_in_min > 60 * 60 * 24:
189 189 return
190 190
191 191 try:
192 192 write()
193 193 except Exception:
194 194 pass
195 195
196 196
197 197 def write_usage_data(event):
198 198 import rhodecode
199 199 from rhodecode.lib import system_info
200 200 from rhodecode.lib import ext_json
201 201
202 202 settings = event.app.registry.settings
203 203 instance_tag = settings.get('metadata.write_usage_tag')
204 204 if not settings.get('metadata.write_usage'):
205 205 return
206 206
207 207 def get_update_age(dest_file):
208 now = datetime.datetime.utcnow()
208 now = datetime.datetime.now(datetime.UTC)
209 209
210 210 with open(dest_file, 'rb') as f:
211 211 data = ext_json.json.loads(f.read())
212 212 if 'created_on' in data:
213 213 update_date = parse(data['created_on'])
214 214 diff = now - update_date
215 215 return math.ceil(diff.total_seconds() / 60.0)
216 216
217 217 return 0
218 218
219 utc_date = datetime.datetime.utcnow()
219 utc_date = datetime.datetime.now(datetime.UTC)
220 220 hour_quarter = int(math.ceil((utc_date.hour + utc_date.minute/60.0) / 6.))
221 fname = '.rc_usage_{date.year}{date.month:02d}{date.day:02d}_{hour}.json'.format(
222 date=utc_date, hour=hour_quarter)
221 fname = f'.rc_usage_{utc_date.year}{utc_date.month:02d}{utc_date.day:02d}_{hour_quarter}.json'
223 222 ini_loc = os.path.dirname(rhodecode.CONFIG.get('__file__'))
224 223
225 224 usage_dir = os.path.join(ini_loc, '.rcusage')
226 225 if not os.path.isdir(usage_dir):
227 226 os.makedirs(usage_dir)
228 227 usage_metadata_destination = os.path.join(usage_dir, fname)
229 228
230 229 try:
231 230 age_in_min = get_update_age(usage_metadata_destination)
232 231 except Exception:
233 232 age_in_min = 0
234 233
235 234 # write every 6th hour
236 235 if age_in_min and age_in_min < 60 * 6:
237 236 log.debug('Usage file created %s minutes ago, skipping (threshold: %s minutes)...',
238 237 age_in_min, 60 * 6)
239 238 return
240 239
241 240 def write(dest_file):
242 241 configuration = system_info.SysInfo(system_info.rhodecode_config)()['value']
243 242 license_token = configuration['config']['license_token']
244 243
245 244 metadata = dict(
246 245 desc='Usage data',
247 246 instance_tag=instance_tag,
248 247 license_token=license_token,
249 248 created_on=datetime.datetime.utcnow().isoformat(),
250 249 usage=system_info.SysInfo(system_info.usage_info)()['value'],
251 250 )
252 251
253 252 with open(dest_file, 'wb') as f:
254 253 f.write(ext_json.formatted_json(metadata))
255 254
256 255 try:
257 256 log.debug('Writing usage file at: %s', usage_metadata_destination)
258 257 write(usage_metadata_destination)
259 258 except Exception:
260 259 pass
261 260
262 261
263 262 def write_js_routes_if_enabled(event):
264 263 registry = event.app.registry
265 264
266 265 mapper = registry.queryUtility(IRoutesMapper)
267 266 _argument_prog = re.compile(r'\{(.*?)\}|:\((.*)\)')
268 267
269 268 def _extract_route_information(route):
270 269 """
271 270 Convert a route into tuple(name, path, args), eg:
272 271 ('show_user', '/profile/%(username)s', ['username'])
273 272 """
274 273
275 274 route_path = route.pattern
276 275 pattern = route.pattern
277 276
278 277 def replace(matchobj):
279 278 if matchobj.group(1):
280 279 return "%%(%s)s" % matchobj.group(1).split(':')[0]
281 280 else:
282 281 return "%%(%s)s" % matchobj.group(2)
283 282
284 283 route_path = _argument_prog.sub(replace, route_path)
285 284
286 285 if not route_path.startswith('/'):
287 286 route_path = f'/{route_path}'
288 287
289 288 return (
290 289 route.name,
291 290 route_path,
292 291 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
293 292 for arg in _argument_prog.findall(pattern)]
294 293 )
295 294
296 295 def get_routes():
297 296 # pyramid routes
298 297 for route in mapper.get_routes():
299 298 if not route.name.startswith('__'):
300 299 yield _extract_route_information(route)
301 300
302 301 if asbool(registry.settings.get('generate_js_files', 'false')):
303 302 static_path = AssetResolver().resolve('rhodecode:public').abspath()
304 303 jsroutes = get_routes()
305 304 jsroutes_file_content = generate_jsroutes_content(jsroutes)
306 305 jsroutes_file_path = os.path.join(
307 306 static_path, 'js', 'rhodecode', 'routes.js')
308 307
309 308 try:
310 309 with open(jsroutes_file_path, 'w', encoding='utf-8') as f:
311 310 f.write(jsroutes_file_content)
312 311 log.debug('generated JS files in %s', jsroutes_file_path)
313 312 except Exception:
314 313 log.exception('Failed to write routes.js into %s', jsroutes_file_path)
315 314
316 315
316 def import_license_if_present(event):
317 """
318 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
319 does a import license key based on a presence of the file.
320 """
321 settings = event.app.registry.settings
322
323 license_file_path = settings.get('license.import_path')
324 force = settings.get('license.import_path_mode') == 'force'
325 if license_file_path:
326 from rhodecode.model.meta import Session
327 from rhodecode.model.license import apply_license_from_file
328 apply_license_from_file(license_file_path, force=force)
329 Session().commit()
330
331
317 332 class Subscriber(object):
318 333 """
319 334 Base class for subscribers to the pyramid event system.
320 335 """
321 336 def __call__(self, event):
322 337 self.run(event)
323 338
324 339 def run(self, event):
325 340 raise NotImplementedError('Subclass has to implement this.')
326 341
327 342
328 343 class AsyncSubscriber(Subscriber):
329 344 """
330 345 Subscriber that handles the execution of events in a separate task to not
331 346 block the execution of the code which triggers the event. It puts the
332 347 received events into a queue from which the worker process takes them in
333 348 order.
334 349 """
335 350 def __init__(self):
336 351 self._stop = False
337 352 self._eventq = queue.Queue()
338 353 self._worker = self.create_worker()
339 354 self._worker.start()
340 355
341 356 def __call__(self, event):
342 357 self._eventq.put(event)
343 358
344 359 def create_worker(self):
345 360 worker = Thread(target=self.do_work)
346 361 worker.daemon = True
347 362 return worker
348 363
349 364 def stop_worker(self):
350 365 self._stop = False
351 366 self._eventq.put(None)
352 367 self._worker.join()
353 368
354 369 def do_work(self):
355 370 while not self._stop:
356 371 event = self._eventq.get()
357 372 if event is not None:
358 373 self.run(event)
359 374
360 375
361 376 class AsyncSubprocessSubscriber(AsyncSubscriber):
362 377 """
363 378 Subscriber that uses the subprocess module to execute a command if an
364 379 event is received. Events are handled asynchronously::
365 380
366 381 subscriber = AsyncSubprocessSubscriber('ls -la', timeout=10)
367 382 subscriber(dummyEvent) # running __call__(event)
368 383
369 384 """
370 385
371 386 def __init__(self, cmd, timeout=None):
372 387 if not isinstance(cmd, (list, tuple)):
373 388 cmd = shlex.split(cmd)
374 389 super().__init__()
375 390 self._cmd = cmd
376 391 self._timeout = timeout
377 392
378 393 def run(self, event):
379 394 cmd = self._cmd
380 395 timeout = self._timeout
381 396 log.debug('Executing command %s.', cmd)
382 397
383 398 try:
384 399 output = subprocess.check_output(
385 400 cmd, timeout=timeout, stderr=subprocess.STDOUT)
386 401 log.debug('Command finished %s', cmd)
387 402 if output:
388 403 log.debug('Command output: %s', output)
389 404 except subprocess.TimeoutExpired as e:
390 405 log.exception('Timeout while executing command.')
391 406 if e.output:
392 407 log.error('Command output: %s', e.output)
393 408 except subprocess.CalledProcessError as e:
394 409 log.exception('Error while executing command.')
395 410 if e.output:
396 411 log.error('Command output: %s', e.output)
397 412 except Exception:
398 413 log.exception(
399 414 'Exception while executing command %s.', cmd)
@@ -1,79 +1,50 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${_('Security Admin')}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="breadcrumbs_links()"></%def>
11 11
12 12 <%def name="menu_bar_nav()">
13 13 ${self.menu_items(active='admin')}
14 14 </%def>
15 15
16 16 <%def name="menu_bar_subnav()">
17 17 ${self.admin_menu(active='security')}
18 18 </%def>
19 19
20 20 <%def name="main()">
21 21
22 22 <div class="box">
23 23
24 24 <div class="panel panel-default">
25 25 <div class="panel-heading">
26 26 <h3 class="panel-title">${_('Security Audit')}</h3>
27 27 </div>
28 28 <div class="panel-body">
29 29 <h4>${_('This feature is available in RhodeCode EE edition only. Contact {sales_email} to obtain a trial license.').format(sales_email='<a href="mailto:sales@rhodecode.com">sales@rhodecode.com</a>')|n}</h4>
30 30 <p>
31 31 ${_('You can scan your repositories for exposed secrets, passwords, etc')}
32 32 </p>
33 33 </div>
34 34 </div>
35 35
36 36 <div class="panel panel-default">
37 37 <div class="panel-heading">
38 38 <h3 class="panel-title">${_('Allowed client versions')}</h3>
39 39 </div>
40 40 <div class="panel-body">
41 %if c.rhodecode_edition_id != 'EE':
42 41 <h4>${_('This feature is available in RhodeCode EE edition only. Contact {sales_email} to obtain a trial license.').format(sales_email='<a href="mailto:sales@rhodecode.com">sales@rhodecode.com</a>')|n}</h4>
43 42 <p>
44 43 ${_('Some outdated client versions may have security vulnerabilities. This section have rules for whitelisting versions of clients for Git, Mercurial and SVN.')}
45 44 </p>
46 %else:
47 <div class="inner form" id="container">
48 </div>
49 %endif
50 45 </div>
51 46
47
52 48 </div>
53 49
54 <script>
55 $(document).ready(function() {
56 $.ajax({
57 url: pyroutes.url('admin_security_modify_allowed_vcs_client_versions'),
58 type: 'GET',
59 success: function(response) {
60 $('#container').html(response);
61 },
62 });
63 $(document).on('submit', '#allowed_clients_form', function(event) {
64 event.preventDefault();
65 var formData = $(this).serialize();
66
67 $.ajax({
68 url: pyroutes.url('admin_security_modify_allowed_vcs_client_versions'),
69 type: 'POST',
70 data: formData,
71 success: function(response) {
72 $('#container').html(response);
73 },
74 });
75 });
76 });
77 </script>
78
79 50 </%def>
@@ -1,323 +1,308 b''
1 1 ## snippet for displaying vcs settings
2 2 ## usage:
3 3 ## <%namespace name="vcss" file="/base/vcssettings.mako"/>
4 4 ## ${vcss.vcs_settings_fields()}
5 5
6 6 <%def name="vcs_settings_fields(suffix='', svn_branch_patterns=None, svn_tag_patterns=None, repo_type=None, display_globals=False, **kwargs)">
7 7 % if display_globals:
8 <div class="panel panel-default">
9 <div class="panel-heading" id="general">
10 <h3 class="panel-title">${_('General')}<a class="permalink" href="#general"></a></h3>
11 </div>
12 <div class="panel-body">
13 <div class="field">
14 <div class="checkbox">
15 ${h.checkbox('web_push_ssl' + suffix, 'True')}
16 <label for="web_push_ssl${suffix}">${_('Require SSL for vcs operations')}</label>
17 </div>
18 <div class="label">
19 <span class="help-block">${_('Activate to set RhodeCode to require SSL for pushing or pulling. If SSL certificate is missing it will return a HTTP Error 406: Not Acceptable.')}</span>
20 </div>
21 </div>
22 </div>
23 </div>
8
24 9 % endif
25 10
26 11 % if display_globals or repo_type in ['git', 'hg']:
27 12 <div class="panel panel-default">
28 13 <div class="panel-heading" id="vcs-hooks-options">
29 14 <h3 class="panel-title">${_('Internal Hooks')}<a class="permalink" href="#vcs-hooks-options"></a></h3>
30 15 </div>
31 16 <div class="panel-body">
32 17 <div class="field">
33 18 <div class="checkbox">
34 19 ${h.checkbox('hooks_changegroup_repo_size' + suffix, 'True', **kwargs)}
35 20 <label for="hooks_changegroup_repo_size${suffix}">${_('Show repository size after push')}</label>
36 21 </div>
37 22
38 23 <div class="label">
39 24 <span class="help-block">${_('Trigger a hook that calculates repository size after each push.')}</span>
40 25 </div>
41 26 <div class="checkbox">
42 27 ${h.checkbox('hooks_changegroup_push_logger' + suffix, 'True', **kwargs)}
43 28 <label for="hooks_changegroup_push_logger${suffix}">${_('Execute pre/post push hooks')}</label>
44 29 </div>
45 30 <div class="label">
46 31 <span class="help-block">${_('Execute Built in pre/post push hooks. This also executes rcextensions hooks.')}</span>
47 32 </div>
48 33 <div class="checkbox">
49 34 ${h.checkbox('hooks_outgoing_pull_logger' + suffix, 'True', **kwargs)}
50 35 <label for="hooks_outgoing_pull_logger${suffix}">${_('Execute pre/post pull hooks')}</label>
51 36 </div>
52 37 <div class="label">
53 38 <span class="help-block">${_('Execute Built in pre/post pull hooks. This also executes rcextensions hooks.')}</span>
54 39 </div>
55 40 </div>
56 41 </div>
57 42 </div>
58 43 % endif
59 44
60 45 % if display_globals or repo_type in ['hg']:
61 46 <div class="panel panel-default">
62 47 <div class="panel-heading" id="vcs-hg-options">
63 48 <h3 class="panel-title">${_('Mercurial Settings')}<a class="permalink" href="#vcs-hg-options"></a></h3>
64 49 </div>
65 50 <div class="panel-body">
66 51 <div class="checkbox">
67 52 ${h.checkbox('extensions_largefiles' + suffix, 'True', **kwargs)}
68 53 <label for="extensions_largefiles${suffix}">${_('Enable largefiles extension')}</label>
69 54 </div>
70 55 <div class="label">
71 56 % if display_globals:
72 57 <span class="help-block">${_('Enable Largefiles extensions for all repositories.')}</span>
73 58 % else:
74 59 <span class="help-block">${_('Enable Largefiles extensions for this repository.')}</span>
75 60 % endif
76 61 </div>
77 62
78 63 <div class="checkbox">
79 64 ${h.checkbox('phases_publish' + suffix, 'True', **kwargs)}
80 65 <label for="phases_publish${suffix}">${_('Set repositories as publishing') if display_globals else _('Set repository as publishing')}</label>
81 66 </div>
82 67 <div class="label">
83 68 <span class="help-block">${_('When this is enabled all commits in the repository are seen as public commits by clients.')}</span>
84 69 </div>
85 70
86 71 <div class="checkbox">
87 72 ${h.checkbox('extensions_evolve' + suffix, 'True', **kwargs)}
88 73 <label for="extensions_evolve${suffix}">${_('Enable Evolve and Topic extension')}</label>
89 74 </div>
90 75 <div class="label">
91 76 % if display_globals:
92 77 <span class="help-block">${_('Enable Evolve and Topic extensions for all repositories.')}</span>
93 78 % else:
94 79 <span class="help-block">${_('Enable Evolve and Topic extensions for this repository.')}</span>
95 80 % endif
96 81 </div>
97 82
98 83 </div>
99 84 </div>
100 85 % endif
101 86
102 87 % if display_globals or repo_type in ['git']:
103 88 <div class="panel panel-default">
104 89 <div class="panel-heading" id="vcs-git-options">
105 90 <h3 class="panel-title">${_('Git Settings')}<a class="permalink" href="#vcs-git-options"></a></h3>
106 91 </div>
107 92 <div class="panel-body">
108 93 <div class="checkbox">
109 94 ${h.checkbox('vcs_git_lfs_enabled' + suffix, 'True', **kwargs)}
110 95 <label for="vcs_git_lfs_enabled${suffix}">${_('Enable lfs extension')}</label>
111 96 </div>
112 97 <div class="label">
113 98 % if display_globals:
114 99 <span class="help-block">${_('Enable lfs extensions for all repositories.')}</span>
115 100 % else:
116 101 <span class="help-block">${_('Enable lfs extensions for this repository.')}</span>
117 102 % endif
118 103 </div>
119 104 </div>
120 105 </div>
121 106 % endif
122 107
123 108 % if display_globals or repo_type in ['svn']:
124 109 <div class="panel panel-default">
125 110 <div class="panel-heading" id="vcs-svn-options">
126 111 <h3 class="panel-title">${_('Subversion Settings')}<a class="permalink" href="#vcs-svn-options"></a></h3>
127 112 </div>
128 113 <div class="panel-body">
129 114 % if display_globals:
130 115 <div class="field">
131 116 <div class="content" >
132 117 <label>${_('mod_dav config')}</label><br/>
133 118 <code>path: ${c.svn_config_path}</code>
134 119 </div>
135 120 <br/>
136 121
137 122 <div>
138 123
139 124 % if c.svn_generate_config:
140 125 <span class="buttons">
141 126 <button class="btn btn-primary" id="vcs_svn_generate_cfg">${_('Re-generate Apache Config')}</button>
142 127 </span>
143 128 % endif
144 129 </div>
145 130 </div>
146 131 % endif
147 132
148 133 <div class="field">
149 134 <div class="content" >
150 135 <label>${_('Repository patterns')}</label><br/>
151 136 </div>
152 137 </div>
153 138 <div class="label">
154 139 <span class="help-block">${_('Patterns for identifying SVN branches and tags. For recursive search, use "*". Eg.: "/branches/*"')}</span>
155 140 </div>
156 141
157 142 <div class="field branch_patterns">
158 143 <div class="input" >
159 144 <label>${_('Branches')}:</label><br/>
160 145 </div>
161 146 % if svn_branch_patterns:
162 147 % for branch in svn_branch_patterns:
163 148 <div class="input adjacent" id="${'id%s' % branch.ui_id}">
164 149 ${h.hidden('branch_ui_key' + suffix, branch.ui_key)}
165 150 ${h.text('branch_value_%d' % branch.ui_id + suffix, branch.ui_value, size=59, readonly="readonly", class_='disabled')}
166 151 % if kwargs.get('disabled') != 'disabled':
167 152 <span class="btn btn-x" onclick="ajaxDeletePattern(${branch.ui_id},'${'id%s' % branch.ui_id}')">
168 153 ${_('Delete')}
169 154 </span>
170 155 % endif
171 156 </div>
172 157 % endfor
173 158 %endif
174 159 </div>
175 160 % if kwargs.get('disabled') != 'disabled':
176 161 <div class="field branch_patterns">
177 162 <div class="input" >
178 163 ${h.text('new_svn_branch',size=59,placeholder='New branch pattern')}
179 164 </div>
180 165 </div>
181 166 % endif
182 167 <div class="field tag_patterns">
183 168 <div class="input" >
184 169 <label>${_('Tags')}:</label><br/>
185 170 </div>
186 171 % if svn_tag_patterns:
187 172 % for tag in svn_tag_patterns:
188 173 <div class="input" id="${'id%s' % tag.ui_id + suffix}">
189 174 ${h.hidden('tag_ui_key' + suffix, tag.ui_key)}
190 175 ${h.text('tag_ui_value_new_%d' % tag.ui_id + suffix, tag.ui_value, size=59, readonly="readonly", class_='disabled tag_input')}
191 176 % if kwargs.get('disabled') != 'disabled':
192 177 <span class="btn btn-x" onclick="ajaxDeletePattern(${tag.ui_id},'${'id%s' % tag.ui_id}')">
193 178 ${_('Delete')}
194 179 </span>
195 180 %endif
196 181 </div>
197 182 % endfor
198 183 % endif
199 184 </div>
200 185 % if kwargs.get('disabled') != 'disabled':
201 186 <div class="field tag_patterns">
202 187 <div class="input" >
203 188 ${h.text('new_svn_tag' + suffix, size=59, placeholder='New tag pattern')}
204 189 </div>
205 190 </div>
206 191 %endif
207 192 </div>
208 193 </div>
209 194 % else:
210 195 ${h.hidden('new_svn_branch' + suffix, '')}
211 196 ${h.hidden('new_svn_tag' + suffix, '')}
212 197 % endif
213 198
214 199
215 200 % if display_globals or repo_type in ['hg', 'git']:
216 201 <div class="panel panel-default">
217 202 <div class="panel-heading" id="vcs-pull-requests-options">
218 203 <h3 class="panel-title">${_('Pull Request Settings')}<a class="permalink" href="#vcs-pull-requests-options"></a></h3>
219 204 </div>
220 205 <div class="panel-body">
221 206 <div class="checkbox">
222 207 ${h.checkbox('rhodecode_pr_merge_enabled' + suffix, 'True', **kwargs)}
223 208 <label for="rhodecode_pr_merge_enabled${suffix}">${_('Enable server-side merge for pull requests')}</label>
224 209 </div>
225 210 <div class="label">
226 211 <span class="help-block">${_('Note: when this feature is enabled, it only runs hooks defined in the rcextension package. Custom hooks added on the Admin -> Settings -> Hooks page will not be run when pull requests are automatically merged from the web interface.')}</span>
227 212 </div>
228 213 <div class="checkbox">
229 214 ${h.checkbox('rhodecode_use_outdated_comments' + suffix, 'True', **kwargs)}
230 215 <label for="rhodecode_use_outdated_comments${suffix}">${_('Invalidate and relocate inline comments during update')}</label>
231 216 </div>
232 217 <div class="label">
233 218 <span class="help-block">${_('During the update of a pull request, the position of inline comments will be updated and outdated inline comments will be hidden.')}</span>
234 219 </div>
235 220 </div>
236 221 </div>
237 222 % endif
238 223
239 224 % if display_globals or repo_type in ['hg', 'git', 'svn']:
240 225 <div class="panel panel-default">
241 226 <div class="panel-heading" id="vcs-pull-requests-options">
242 227 <h3 class="panel-title">${_('Diff cache')}<a class="permalink" href="#vcs-pull-requests-options"></a></h3>
243 228 </div>
244 229 <div class="panel-body">
245 230 <div class="checkbox">
246 231 ${h.checkbox('rhodecode_diff_cache' + suffix, 'True', **kwargs)}
247 232 <label for="rhodecode_diff_cache${suffix}">${_('Enable caching diffs for pull requests cache and commits')}</label>
248 233 </div>
249 234 </div>
250 235 </div>
251 236 % endif
252 237
253 238 % if display_globals or repo_type in ['hg',]:
254 239 <div class="panel panel-default">
255 240 <div class="panel-heading" id="vcs-pull-requests-options">
256 241 <h3 class="panel-title">${_('Mercurial Pull Request Settings')}<a class="permalink" href="#vcs-hg-pull-requests-options"></a></h3>
257 242 </div>
258 243 <div class="panel-body">
259 244 ## Specific HG settings
260 245 <div class="checkbox">
261 246 ${h.checkbox('rhodecode_hg_use_rebase_for_merging' + suffix, 'True', **kwargs)}
262 247 <label for="rhodecode_hg_use_rebase_for_merging${suffix}">${_('Use rebase as merge strategy')}</label>
263 248 </div>
264 249 <div class="label">
265 250 <span class="help-block">${_('Use rebase instead of creating a merge commit when merging via web interface.')}</span>
266 251 </div>
267 252
268 253 <div class="checkbox">
269 254 ${h.checkbox('rhodecode_hg_close_branch_before_merging' + suffix, 'True', **kwargs)}
270 255 <label for="rhodecode_hg_close_branch_before_merging{suffix}">${_('Close branch before merging it')}</label>
271 256 </div>
272 257 <div class="label">
273 258 <span class="help-block">${_('Close branch before merging it into destination branch. No effect when rebase strategy is use.')}</span>
274 259 </div>
275 260
276 261
277 262 </div>
278 263 </div>
279 264 % endif
280 265
281 266 % if display_globals or repo_type in ['git']:
282 267 <div class="panel panel-default">
283 268 <div class="panel-heading" id="vcs-pull-requests-options">
284 269 <h3 class="panel-title">${_('Git Pull Request Settings')}<a class="permalink" href="#vcs-git-pull-requests-options"></a></h3>
285 270 </div>
286 271 <div class="panel-body">
287 272 ## <div class="checkbox">
288 273 ## ${h.checkbox('rhodecode_git_use_rebase_for_merging' + suffix, 'True', **kwargs)}
289 274 ## <label for="rhodecode_git_use_rebase_for_merging${suffix}">${_('Use rebase as merge strategy')}</label>
290 275 ## </div>
291 276 ## <div class="label">
292 277 ## <span class="help-block">${_('Use rebase instead of creating a merge commit when merging via web interface.')}</span>
293 278 ## </div>
294 279
295 280 <div class="checkbox">
296 281 ${h.checkbox('rhodecode_git_close_branch_before_merging' + suffix, 'True', **kwargs)}
297 282 <label for="rhodecode_git_close_branch_before_merging{suffix}">${_('Delete branch after merging it')}</label>
298 283 </div>
299 284 <div class="label">
300 285 <span class="help-block">${_('Delete branch after merging it into destination branch.')}</span>
301 286 </div>
302 287 </div>
303 288 </div>
304 289 % endif
305 290
306 291 <script type="text/javascript">
307 292
308 293 $(document).ready(function() {
309 294 /* On click handler for the `Generate Apache Config` button. It sends a
310 295 POST request to trigger the (re)generation of the mod_dav_svn config. */
311 296 $('#vcs_svn_generate_cfg').on('click', function(event) {
312 297 event.preventDefault();
313 298 var url = "${h.route_path('admin_settings_vcs_svn_generate_cfg')}";
314 299 var jqxhr = $.post(url, {'csrf_token': CSRF_TOKEN});
315 300 jqxhr.done(function(data) {
316 301 $.Topic('/notifications').publish(data);
317 302 });
318 303 });
319 304 });
320 305
321 306 </script>
322 307 </%def>
323 308
@@ -1,155 +1,154 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import urllib.parse
21 21
22 22 import mock
23 23 import pytest
24 24 import simplejson as json
25 25
26 26 from rhodecode.lib.vcs.backends.base import Config
27 27 from rhodecode.tests.lib.middleware import mock_scm_app
28 28 import rhodecode.lib.middleware.simplehg as simplehg
29 29
30 30
31 31 def get_environ(url):
32 32 """Construct a minimum WSGI environ based on the URL."""
33 33 parsed_url = urllib.parse.urlparse(url)
34 34 environ = {
35 35 'PATH_INFO': parsed_url.path,
36 36 'QUERY_STRING': parsed_url.query,
37 37 }
38 38
39 39 return environ
40 40
41 41
42 42 @pytest.mark.parametrize(
43 43 'url, expected_action',
44 44 [
45 45 ('/foo/bar?cmd=unbundle&key=tip', 'push'),
46 46 ('/foo/bar?cmd=pushkey&key=tip', 'push'),
47 47 ('/foo/bar?cmd=listkeys&key=tip', 'pull'),
48 48 ('/foo/bar?cmd=changegroup&key=tip', 'pull'),
49 49 ('/foo/bar?cmd=hello', 'pull'),
50 50 ('/foo/bar?cmd=batch', 'push'),
51 51 ('/foo/bar?cmd=putlfile', 'push'),
52 52 # Edge case: unknown argument: assume push
53 53 ('/foo/bar?cmd=unknown&key=tip', 'push'),
54 54 ('/foo/bar?cmd=&key=tip', 'push'),
55 55 # Edge case: not cmd argument
56 56 ('/foo/bar?key=tip', 'push'),
57 57 ])
58 58 def test_get_action(url, expected_action, request_stub):
59 59 app = simplehg.SimpleHg(config={'auth_ret_code': '', 'base_path': ''},
60 60 registry=request_stub.registry)
61 61 assert expected_action == app._get_action(get_environ(url))
62 62
63 63
64 64 @pytest.mark.parametrize(
65 65 'environ, expected_xargs, expected_batch',
66 66 [
67 67 ({},
68 68 [''], ['push']),
69 69
70 70 ({'HTTP_X_HGARG_1': ''},
71 71 [''], ['push']),
72 72
73 73 ({'HTTP_X_HGARG_1': 'cmds=listkeys+namespace%3Dphases'},
74 74 ['listkeys namespace=phases'], ['pull']),
75 75
76 76 ({'HTTP_X_HGARG_1': 'cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'},
77 77 ['pushkey namespace=bookmarks,key=bm,old=,new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'], ['push']),
78 78
79 79 ({'HTTP_X_HGARG_1': 'namespace=phases'},
80 80 ['namespace=phases'], ['push']),
81 81
82 82 ])
83 83 def test_xarg_and_batch_commands(environ, expected_xargs, expected_batch):
84 84 app = simplehg.SimpleHg
85 85
86 86 result = app._get_xarg_headers(environ)
87 87 result_batch = app._get_batch_cmd(environ)
88 88 assert expected_xargs == result
89 89 assert expected_batch == result_batch
90 90
91 91
92 92 @pytest.mark.parametrize(
93 93 'url, expected_repo_name',
94 94 [
95 95 ('/foo?cmd=unbundle&key=tip', 'foo'),
96 96 ('/foo/bar?cmd=pushkey&key=tip', 'foo/bar'),
97 97 ('/foo/bar/baz?cmd=listkeys&key=tip', 'foo/bar/baz'),
98 98 # Repos with trailing slashes.
99 99 ('/foo/?cmd=unbundle&key=tip', 'foo'),
100 100 ('/foo/bar/?cmd=pushkey&key=tip', 'foo/bar'),
101 101 ('/foo/bar/baz/?cmd=listkeys&key=tip', 'foo/bar/baz'),
102 102 ])
103 103 def test_get_repository_name(url, expected_repo_name, request_stub):
104 104 app = simplehg.SimpleHg(config={'auth_ret_code': '', 'base_path': ''},
105 105 registry=request_stub.registry)
106 106 assert expected_repo_name == app._get_repository_name(get_environ(url))
107 107
108 108
109 109 def test_get_config(user_util, baseapp, request_stub):
110 110 repo = user_util.create_repo(repo_type='git')
111 111 app = simplehg.SimpleHg(config={'auth_ret_code': '', 'base_path': ''},
112 112 registry=request_stub.registry)
113 113 extras = [('foo', 'FOO', 'bar', 'BAR')]
114 114
115 115 hg_config = app._create_config(extras, repo_name=repo.repo_name)
116 116
117 117 config = simplehg.utils.make_db_config(repo=repo.repo_name)
118 118 config.set('rhodecode', 'RC_SCM_DATA', json.dumps(extras))
119 119 hg_config_org = config
120 120
121 121 expected_config = [
122 122 ('vcs_svn_tag', 'ff89f8c714d135d865f44b90e5413b88de19a55f', '/tags/*'),
123 ('web', 'push_ssl', 'False'),
124 123 ('web', 'allow_push', '*'),
125 124 ('web', 'allow_archive', 'gz zip bz2'),
126 125 ('web', 'baseurl', '/'),
127 126 ('vcs_git_lfs', 'store_location', hg_config_org.get('vcs_git_lfs', 'store_location')),
128 127 ('vcs_svn_branch', '9aac1a38c3b8a0cdc4ae0f960a5f83332bc4fa5e', '/branches/*'),
129 128 ('vcs_svn_branch', 'c7e6a611c87da06529fd0dd733308481d67c71a8', '/trunk'),
130 129 ('largefiles', 'usercache', hg_config_org.get('largefiles', 'usercache')),
131 130 ('hooks', 'preoutgoing.pre_pull', 'python:vcsserver.hooks.pre_pull'),
132 131 ('hooks', 'prechangegroup.pre_push', 'python:vcsserver.hooks.pre_push'),
133 132 ('hooks', 'outgoing.pull_logger', 'python:vcsserver.hooks.log_pull_action'),
134 133 ('hooks', 'pretxnchangegroup.pre_push', 'python:vcsserver.hooks.pre_push'),
135 134 ('hooks', 'changegroup.push_logger', 'python:vcsserver.hooks.log_push_action'),
136 135 ('hooks', 'changegroup.repo_size', 'python:vcsserver.hooks.repo_size'),
137 136 ('phases', 'publish', 'True'),
138 137 ('extensions', 'largefiles', ''),
139 138 ('paths', '/', hg_config_org.get('paths', '/')),
140 139 ('rhodecode', 'RC_SCM_DATA', '[["foo","FOO","bar","BAR"]]')
141 140 ]
142 141 for entry in expected_config:
143 142 assert entry in hg_config
144 143
145 144
146 145 def test_create_wsgi_app_uses_scm_app_from_simplevcs(request_stub):
147 146 config = {
148 147 'auth_ret_code': '',
149 148 'base_path': '',
150 149 'vcs.scm_app_implementation':
151 150 'rhodecode.tests.lib.middleware.mock_scm_app',
152 151 }
153 152 app = simplehg.SimpleHg(config=config, registry=request_stub.registry)
154 153 wsgi_app = app._create_wsgi_app('/tmp/test', 'test_repo', {})
155 154 assert wsgi_app is mock_scm_app.mock_hg_wsgi
@@ -1,451 +1,448 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import mock
21 21 import pytest
22 22
23 23 from rhodecode.lib.str_utils import base64_to_str
24 24 from rhodecode.lib.utils2 import AttributeDict
25 25 from rhodecode.tests.utils import CustomTestApp
26 26
27 27 from rhodecode.lib.caching_query import FromCache
28 28 from rhodecode.lib.middleware import simplevcs
29 29 from rhodecode.lib.middleware.https_fixup import HttpsFixup
30 30 from rhodecode.lib.middleware.utils import scm_app_http
31 31 from rhodecode.model.db import User, _hash_key
32 32 from rhodecode.model.meta import Session, cache as db_cache
33 33 from rhodecode.tests import (
34 34 HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
35 35 from rhodecode.tests.lib.middleware import mock_scm_app
36 36
37 37
38 38 class StubVCSController(simplevcs.SimpleVCS):
39 39
40 40 SCM = 'hg'
41 41 stub_response_body = tuple()
42 42
43 43 def __init__(self, *args, **kwargs):
44 44 super(StubVCSController, self).__init__(*args, **kwargs)
45 45 self._action = 'pull'
46 46 self._is_shadow_repo_dir = True
47 47 self._name = HG_REPO
48 48 self.set_repo_names(None)
49 49
50 50 @property
51 51 def is_shadow_repo_dir(self):
52 52 return self._is_shadow_repo_dir
53 53
54 54 def _get_repository_name(self, environ):
55 55 return self._name
56 56
57 57 def _get_action(self, environ):
58 58 return self._action
59 59
60 60 def _create_wsgi_app(self, repo_path, repo_name, config):
61 61 def fake_app(environ, start_response):
62 62 headers = [
63 63 ('Http-Accept', 'application/mercurial')
64 64 ]
65 65 start_response('200 OK', headers)
66 66 return self.stub_response_body
67 67 return fake_app
68 68
69 69 def _create_config(self, extras, repo_name, scheme='http'):
70 70 return None
71 71
72 72
73 73 @pytest.fixture()
74 74 def vcscontroller(baseapp, config_stub, request_stub):
75 75 from rhodecode.config.middleware import ce_auth_resources
76 76
77 77 config_stub.testing_securitypolicy()
78 78 config_stub.include('rhodecode.authentication')
79 79
80 80 for resource in ce_auth_resources:
81 81 config_stub.include(resource)
82 82
83 83 controller = StubVCSController(
84 84 baseapp.config.get_settings(), request_stub.registry)
85 85 app = HttpsFixup(controller, baseapp.config.get_settings())
86 86 app = CustomTestApp(app)
87 87
88 88 _remove_default_user_from_query_cache()
89 89
90 90 # Sanity checks that things are set up correctly
91 91 app.get('/' + HG_REPO, status=200)
92 92
93 93 app.controller = controller
94 94 return app
95 95
96 96
97 97 def _remove_default_user_from_query_cache():
98 98 user = User.get_default_user(cache=True)
99 99 query = Session().query(User).filter(User.username == user.username)
100 100 query = query.options(
101 101 FromCache("sql_cache_short", f"get_user_{_hash_key(user.username)}"))
102 102
103 103 db_cache.invalidate(
104 104 query, {},
105 105 FromCache("sql_cache_short", f"get_user_{_hash_key(user.username)}"))
106 106
107 107 Session().expire(user)
108 108
109 109
110 110 def test_handles_exceptions_during_permissions_checks(
111 111 vcscontroller, disable_anonymous_user, enable_auth_plugins, test_user_factory):
112 112
113 113 test_password = 'qweqwe'
114 114 test_user = test_user_factory(password=test_password, extern_type='headers', extern_name='headers')
115 115 test_username = test_user.username
116 116
117 117 enable_auth_plugins.enable([
118 118 'egg:rhodecode-enterprise-ce#headers',
119 119 'egg:rhodecode-enterprise-ce#token',
120 120 'egg:rhodecode-enterprise-ce#rhodecode'],
121 121 override={
122 122 'egg:rhodecode-enterprise-ce#headers': {'auth_headers_header': 'REMOTE_USER'}
123 123 })
124 124
125 125 user_and_pass = f'{test_username}:{test_password}'
126 126 auth_password = base64_to_str(user_and_pass)
127 127
128 128 extra_environ = {
129 129 'AUTH_TYPE': 'Basic',
130 130 'HTTP_AUTHORIZATION': f'Basic {auth_password}',
131 131 'REMOTE_USER': test_username,
132 132 }
133 133
134 134 # Verify that things are hooked up correctly, we pass user with headers bound auth, and headers filled in
135 135 vcscontroller.get('/', status=200, extra_environ=extra_environ)
136 136
137 137 # Simulate trouble during permission checks
138 138 with mock.patch('rhodecode.model.db.User.get_by_username',
139 139 side_effect=Exception('permission_error_test')) as get_user:
140 140 # Verify that a correct 500 is returned and check that the expected
141 141 # code path was hit.
142 142 vcscontroller.get('/', status=500, extra_environ=extra_environ)
143 143 assert get_user.called
144 144
145 145
146 146 class StubFailVCSController(simplevcs.SimpleVCS):
147 147 def _handle_request(self, environ, start_response):
148 148 raise Exception("BOOM")
149 149
150 150
151 151 @pytest.fixture(scope='module')
152 152 def fail_controller(baseapp):
153 153 controller = StubFailVCSController(
154 154 baseapp.config.get_settings(), baseapp.config)
155 155 controller = HttpsFixup(controller, baseapp.config.get_settings())
156 156 controller = CustomTestApp(controller)
157 157 return controller
158 158
159 159
160 160 def test_handles_exceptions_as_internal_server_error(fail_controller):
161 161 fail_controller.get('/', status=500)
162 162
163 163
164 164 def test_provides_traceback_for_appenlight(fail_controller):
165 165 response = fail_controller.get(
166 166 '/', status=500, extra_environ={'appenlight.client': 'fake'})
167 167 assert 'appenlight.__traceback' in response.request.environ
168 168
169 169
170 170 def test_provides_utils_scm_app_as_scm_app_by_default(baseapp, request_stub):
171 171 controller = StubVCSController(baseapp.config.get_settings(), request_stub.registry)
172 172 assert controller.scm_app is scm_app_http
173 173
174 174
175 175 def test_allows_to_override_scm_app_via_config(baseapp, request_stub):
176 176 config = baseapp.config.get_settings().copy()
177 177 config['vcs.scm_app_implementation'] = (
178 178 'rhodecode.tests.lib.middleware.mock_scm_app')
179 179 controller = StubVCSController(config, request_stub.registry)
180 180 assert controller.scm_app is mock_scm_app
181 181
182 182
183 183 @pytest.mark.parametrize('query_string, expected', [
184 184 ('cmd=stub_command', True),
185 185 ('cmd=listkeys', False),
186 186 ])
187 187 def test_should_check_locking(query_string, expected):
188 188 result = simplevcs._should_check_locking(query_string)
189 189 assert result == expected
190 190
191 191
192 192 class TestShadowRepoRegularExpression(object):
193 193 pr_segment = 'pull-request'
194 194 shadow_segment = 'repository'
195 195
196 196 @pytest.mark.parametrize('url, expected', [
197 197 # repo with/without groups
198 198 ('My-Repo/{pr_segment}/1/{shadow_segment}', True),
199 199 ('Group/My-Repo/{pr_segment}/2/{shadow_segment}', True),
200 200 ('Group/Sub-Group/My-Repo/{pr_segment}/3/{shadow_segment}', True),
201 201 ('Group/Sub-Group1/Sub-Group2/My-Repo/{pr_segment}/3/{shadow_segment}', True),
202 202
203 203 # pull request ID
204 204 ('MyRepo/{pr_segment}/1/{shadow_segment}', True),
205 205 ('MyRepo/{pr_segment}/1234567890/{shadow_segment}', True),
206 206 ('MyRepo/{pr_segment}/-1/{shadow_segment}', False),
207 207 ('MyRepo/{pr_segment}/invalid/{shadow_segment}', False),
208 208
209 209 # unicode
210 210 (u'Sp€çîál-Repö/{pr_segment}/1/{shadow_segment}', True),
211 211 (u'Sp€çîál-Gröüp/Sp€çîál-Repö/{pr_segment}/1/{shadow_segment}', True),
212 212
213 213 # trailing/leading slash
214 214 ('/My-Repo/{pr_segment}/1/{shadow_segment}', False),
215 215 ('My-Repo/{pr_segment}/1/{shadow_segment}/', False),
216 216 ('/My-Repo/{pr_segment}/1/{shadow_segment}/', False),
217 217
218 218 # misc
219 219 ('My-Repo/{pr_segment}/1/{shadow_segment}/extra', False),
220 220 ('My-Repo/{pr_segment}/1/{shadow_segment}extra', False),
221 221 ])
222 222 def test_shadow_repo_regular_expression(self, url, expected):
223 223 from rhodecode.lib.middleware.simplevcs import SimpleVCS
224 224 url = url.format(
225 225 pr_segment=self.pr_segment,
226 226 shadow_segment=self.shadow_segment)
227 227 match_obj = SimpleVCS.shadow_repo_re.match(url)
228 228 assert (match_obj is not None) == expected
229 229
230 230
231 231 @pytest.mark.backends('git', 'hg')
232 232 class TestShadowRepoExposure(object):
233 233
234 234 def test_pull_on_shadow_repo_propagates_to_wsgi_app(
235 235 self, baseapp, request_stub):
236 236 """
237 237 Check that a pull action to a shadow repo is propagated to the
238 238 underlying wsgi app.
239 239 """
240 240 controller = StubVCSController(
241 241 baseapp.config.get_settings(), request_stub.registry)
242 controller._check_ssl = mock.Mock()
243 242 controller.is_shadow_repo = True
244 243 controller._action = 'pull'
245 244 controller._is_shadow_repo_dir = True
246 245 controller.stub_response_body = (b'dummy body value',)
247 246 controller._get_default_cache_ttl = mock.Mock(
248 247 return_value=(False, 0))
249 248
250 249 environ_stub = {
251 250 'HTTP_HOST': 'test.example.com',
252 251 'HTTP_ACCEPT': 'application/mercurial',
253 252 'REQUEST_METHOD': 'GET',
254 253 'wsgi.url_scheme': 'http',
255 254 }
256 255
257 256 response = controller(environ_stub, mock.Mock())
258 257 response_body = b''.join(response)
259 258
260 259 # Assert that we got the response from the wsgi app.
261 260 assert response_body == b''.join(controller.stub_response_body)
262 261
263 262 def test_pull_on_shadow_repo_that_is_missing(self, baseapp, request_stub):
264 263 """
265 264 Check that a pull action to a shadow repo is propagated to the
266 265 underlying wsgi app.
267 266 """
268 267 controller = StubVCSController(
269 268 baseapp.config.get_settings(), request_stub.registry)
270 controller._check_ssl = mock.Mock()
271 269 controller.is_shadow_repo = True
272 270 controller._action = 'pull'
273 271 controller._is_shadow_repo_dir = False
274 272 controller.stub_response_body = (b'dummy body value',)
275 273 environ_stub = {
276 274 'HTTP_HOST': 'test.example.com',
277 275 'HTTP_ACCEPT': 'application/mercurial',
278 276 'REQUEST_METHOD': 'GET',
279 277 'wsgi.url_scheme': 'http',
280 278 }
281 279
282 280 response = controller(environ_stub, mock.Mock())
283 281 response_body = b''.join(response)
284 282
285 283 # Assert that we got the response from the wsgi app.
286 284 assert b'404 Not Found' in response_body
287 285
288 286 def test_push_on_shadow_repo_raises(self, baseapp, request_stub):
289 287 """
290 288 Check that a push action to a shadow repo is aborted.
291 289 """
292 290 controller = StubVCSController(
293 291 baseapp.config.get_settings(), request_stub.registry)
294 controller._check_ssl = mock.Mock()
295 292 controller.is_shadow_repo = True
296 293 controller._action = 'push'
297 294 controller.stub_response_body = (b'dummy body value',)
298 295 environ_stub = {
299 296 'HTTP_HOST': 'test.example.com',
300 297 'HTTP_ACCEPT': 'application/mercurial',
301 298 'REQUEST_METHOD': 'GET',
302 299 'wsgi.url_scheme': 'http',
303 300 }
304 301
305 302 response = controller(environ_stub, mock.Mock())
306 303 response_body = b''.join(response)
307 304
308 305 assert response_body != controller.stub_response_body
309 306 # Assert that a 406 error is returned.
310 307 assert b'406 Not Acceptable' in response_body
311 308
312 309 def test_set_repo_names_no_shadow(self, baseapp, request_stub):
313 310 """
314 311 Check that the set_repo_names method sets all names to the one returned
315 312 by the _get_repository_name method on a request to a non shadow repo.
316 313 """
317 314 environ_stub = {}
318 315 controller = StubVCSController(
319 316 baseapp.config.get_settings(), request_stub.registry)
320 317 controller._name = 'RepoGroup/MyRepo'
321 318 controller.set_repo_names(environ_stub)
322 319 assert not controller.is_shadow_repo
323 320 assert (controller.url_repo_name ==
324 321 controller.acl_repo_name ==
325 322 controller.vcs_repo_name ==
326 323 controller._get_repository_name(environ_stub))
327 324
328 325 def test_set_repo_names_with_shadow(
329 326 self, baseapp, pr_util, config_stub, request_stub):
330 327 """
331 328 Check that the set_repo_names method sets correct names on a request
332 329 to a shadow repo.
333 330 """
334 331 from rhodecode.model.pull_request import PullRequestModel
335 332
336 333 pull_request = pr_util.create_pull_request()
337 334 shadow_url = '{target}/{pr_segment}/{pr_id}/{shadow_segment}'.format(
338 335 target=pull_request.target_repo.repo_name,
339 336 pr_id=pull_request.pull_request_id,
340 337 pr_segment=TestShadowRepoRegularExpression.pr_segment,
341 338 shadow_segment=TestShadowRepoRegularExpression.shadow_segment)
342 339 controller = StubVCSController(
343 340 baseapp.config.get_settings(), request_stub.registry)
344 341 controller._name = shadow_url
345 342 controller.set_repo_names({})
346 343
347 344 # Get file system path to shadow repo for assertions.
348 345 workspace_id = PullRequestModel()._workspace_id(pull_request)
349 346 vcs_repo_name = pull_request.target_repo.get_shadow_repository_path(workspace_id)
350 347
351 348 assert controller.vcs_repo_name == vcs_repo_name
352 349 assert controller.url_repo_name == shadow_url
353 350 assert controller.acl_repo_name == pull_request.target_repo.repo_name
354 351 assert controller.is_shadow_repo
355 352
356 353 def test_set_repo_names_with_shadow_but_missing_pr(
357 354 self, baseapp, pr_util, config_stub, request_stub):
358 355 """
359 356 Checks that the set_repo_names method enforces matching target repos
360 357 and pull request IDs.
361 358 """
362 359 pull_request = pr_util.create_pull_request()
363 360 shadow_url = '{target}/{pr_segment}/{pr_id}/{shadow_segment}'.format(
364 361 target=pull_request.target_repo.repo_name,
365 362 pr_id=999999999,
366 363 pr_segment=TestShadowRepoRegularExpression.pr_segment,
367 364 shadow_segment=TestShadowRepoRegularExpression.shadow_segment)
368 365 controller = StubVCSController(
369 366 baseapp.config.get_settings(), request_stub.registry)
370 367 controller._name = shadow_url
371 368 controller.set_repo_names({})
372 369
373 370 assert not controller.is_shadow_repo
374 371 assert (controller.url_repo_name ==
375 372 controller.acl_repo_name ==
376 373 controller.vcs_repo_name)
377 374
378 375
379 376 @pytest.mark.usefixtures('baseapp')
380 377 class TestGenerateVcsResponse(object):
381 378
382 379 def test_ensures_that_start_response_is_called_early_enough(self):
383 380 self.call_controller_with_response_body(iter(['a', 'b']))
384 381 assert self.start_response.called
385 382
386 383 def test_invalidates_cache_after_body_is_consumed(self):
387 384 result = self.call_controller_with_response_body(iter(['a', 'b']))
388 385 assert not self.was_cache_invalidated()
389 386 # Consume the result
390 387 list(result)
391 388 assert self.was_cache_invalidated()
392 389
393 390 def test_raises_unknown_exceptions(self):
394 391 result = self.call_controller_with_response_body(
395 392 self.raise_result_iter(vcs_kind='unknown'))
396 393 with pytest.raises(Exception):
397 394 list(result)
398 395
399 396 def call_controller_with_response_body(self, response_body):
400 397 settings = {
401 398 'base_path': 'fake_base_path',
402 399 'vcs.hooks.protocol.v2': 'celery',
403 400 'vcs.hooks.direct_calls': False,
404 401 }
405 402 registry = AttributeDict()
406 403 controller = StubVCSController(settings, registry)
407 404 controller._invalidate_cache = mock.Mock()
408 405 controller.stub_response_body = response_body
409 406 self.start_response = mock.Mock()
410 407 result = controller._generate_vcs_response(
411 408 environ={}, start_response=self.start_response,
412 409 repo_path='fake_repo_path',
413 410 extras={}, action='push')
414 411 self.controller = controller
415 412 return result
416 413
417 414 def raise_result_iter(self, vcs_kind='repo_locked'):
418 415 """
419 416 Simulates an exception due to a vcs raised exception if kind vcs_kind
420 417 """
421 418 raise self.vcs_exception(vcs_kind=vcs_kind)
422 419 yield "never_reached"
423 420
424 421 def vcs_exception(self, vcs_kind='repo_locked'):
425 422 locked_exception = Exception('TEST_MESSAGE')
426 423 locked_exception._vcs_kind = vcs_kind
427 424 return locked_exception
428 425
429 426 def was_cache_invalidated(self):
430 427 return self.controller._invalidate_cache.called
431 428
432 429
433 430 class TestInitializeGenerator(object):
434 431
435 432 def test_drains_first_element(self):
436 433 gen = self.factory(['__init__', 1, 2])
437 434 result = list(gen)
438 435 assert result == [1, 2]
439 436
440 437 @pytest.mark.parametrize('values', [
441 438 [],
442 439 [1, 2],
443 440 ])
444 441 def test_raises_value_error(self, values):
445 442 with pytest.raises(ValueError):
446 443 self.factory(values)
447 444
448 445 @simplevcs.initialize_generator
449 446 def factory(self, iterable):
450 447 for elem in iterable:
451 448 yield elem
@@ -1,1108 +1,1097 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import mock
21 21 import pytest
22 22
23 23 from rhodecode.lib.utils2 import str2bool
24 24 from rhodecode.model.meta import Session
25 25 from rhodecode.model.settings import VcsSettingsModel, UiSetting
26 26
27 27
28 28 HOOKS_FORM_DATA = {
29 29 'hooks_changegroup_repo_size': True,
30 30 'hooks_changegroup_push_logger': True,
31 31 'hooks_outgoing_pull_logger': True
32 32 }
33 33
34 34 SVN_FORM_DATA = {
35 35 'new_svn_branch': 'test-branch',
36 36 'new_svn_tag': 'test-tag'
37 37 }
38 38
39 39 GENERAL_FORM_DATA = {
40 40 'rhodecode_pr_merge_enabled': True,
41 41 'rhodecode_use_outdated_comments': True,
42 42 'rhodecode_hg_use_rebase_for_merging': True,
43 43 'rhodecode_hg_close_branch_before_merging': True,
44 44 'rhodecode_git_use_rebase_for_merging': True,
45 45 'rhodecode_git_close_branch_before_merging': True,
46 46 'rhodecode_diff_cache': True,
47 47 }
48 48
49 49
50 50 class TestInheritGlobalSettingsProperty(object):
51 51 def test_get_raises_exception_when_repository_not_specified(self):
52 52 model = VcsSettingsModel()
53 53 with pytest.raises(Exception) as exc_info:
54 54 model.inherit_global_settings
55 55 assert str(exc_info.value) == 'Repository is not specified'
56 56
57 57 def test_true_is_returned_when_value_is_not_found(self, repo_stub):
58 58 model = VcsSettingsModel(repo=repo_stub.repo_name)
59 59 assert model.inherit_global_settings is True
60 60
61 61 def test_value_is_returned(self, repo_stub, settings_util):
62 62 model = VcsSettingsModel(repo=repo_stub.repo_name)
63 63 settings_util.create_repo_rhodecode_setting(
64 64 repo_stub, VcsSettingsModel.INHERIT_SETTINGS, False, 'bool')
65 65 assert model.inherit_global_settings is False
66 66
67 67 def test_value_is_set(self, repo_stub):
68 68 model = VcsSettingsModel(repo=repo_stub.repo_name)
69 69 model.inherit_global_settings = False
70 70 setting = model.repo_settings.get_setting_by_name(
71 71 VcsSettingsModel.INHERIT_SETTINGS)
72 72 try:
73 73 assert setting.app_settings_type == 'bool'
74 74 assert setting.app_settings_value is False
75 75 finally:
76 76 Session().delete(setting)
77 77 Session().commit()
78 78
79 79 def test_set_raises_exception_when_repository_not_specified(self):
80 80 model = VcsSettingsModel()
81 81 with pytest.raises(Exception) as exc_info:
82 82 model.inherit_global_settings = False
83 83 assert str(exc_info.value) == 'Repository is not specified'
84 84
85 85
86 86 class TestVcsSettingsModel(object):
87 87 def test_global_svn_branch_patterns(self):
88 88 model = VcsSettingsModel()
89 89 expected_result = {'test': 'test'}
90 90 with mock.patch.object(model, 'global_settings') as settings_mock:
91 91 get_settings = settings_mock.get_ui_by_section
92 92 get_settings.return_value = expected_result
93 93 settings_mock.return_value = expected_result
94 94 result = model.get_global_svn_branch_patterns()
95 95
96 96 get_settings.assert_called_once_with(model.SVN_BRANCH_SECTION)
97 97 assert expected_result == result
98 98
99 99 def test_repo_svn_branch_patterns(self):
100 100 model = VcsSettingsModel()
101 101 expected_result = {'test': 'test'}
102 102 with mock.patch.object(model, 'repo_settings') as settings_mock:
103 103 get_settings = settings_mock.get_ui_by_section
104 104 get_settings.return_value = expected_result
105 105 settings_mock.return_value = expected_result
106 106 result = model.get_repo_svn_branch_patterns()
107 107
108 108 get_settings.assert_called_once_with(model.SVN_BRANCH_SECTION)
109 109 assert expected_result == result
110 110
111 111 def test_repo_svn_branch_patterns_raises_exception_when_repo_is_not_set(
112 112 self):
113 113 model = VcsSettingsModel()
114 114 with pytest.raises(Exception) as exc_info:
115 115 model.get_repo_svn_branch_patterns()
116 116 assert str(exc_info.value) == 'Repository is not specified'
117 117
118 118 def test_global_svn_tag_patterns(self):
119 119 model = VcsSettingsModel()
120 120 expected_result = {'test': 'test'}
121 121 with mock.patch.object(model, 'global_settings') as settings_mock:
122 122 get_settings = settings_mock.get_ui_by_section
123 123 get_settings.return_value = expected_result
124 124 settings_mock.return_value = expected_result
125 125 result = model.get_global_svn_tag_patterns()
126 126
127 127 get_settings.assert_called_once_with(model.SVN_TAG_SECTION)
128 128 assert expected_result == result
129 129
130 130 def test_repo_svn_tag_patterns(self):
131 131 model = VcsSettingsModel()
132 132 expected_result = {'test': 'test'}
133 133 with mock.patch.object(model, 'repo_settings') as settings_mock:
134 134 get_settings = settings_mock.get_ui_by_section
135 135 get_settings.return_value = expected_result
136 136 settings_mock.return_value = expected_result
137 137 result = model.get_repo_svn_tag_patterns()
138 138
139 139 get_settings.assert_called_once_with(model.SVN_TAG_SECTION)
140 140 assert expected_result == result
141 141
142 142 def test_repo_svn_tag_patterns_raises_exception_when_repo_is_not_set(self):
143 143 model = VcsSettingsModel()
144 144 with pytest.raises(Exception) as exc_info:
145 145 model.get_repo_svn_tag_patterns()
146 146 assert str(exc_info.value) == 'Repository is not specified'
147 147
148 148 def test_get_global_settings(self):
149 149 expected_result = {'test': 'test'}
150 150 model = VcsSettingsModel()
151 151 with mock.patch.object(model, '_collect_all_settings') as collect_mock:
152 152 collect_mock.return_value = expected_result
153 153 result = model.get_global_settings()
154 154
155 155 collect_mock.assert_called_once_with(global_=True)
156 156 assert result == expected_result
157 157
158 158 def test_get_repo_settings(self, repo_stub):
159 159 model = VcsSettingsModel(repo=repo_stub.repo_name)
160 160 expected_result = {'test': 'test'}
161 161 with mock.patch.object(model, '_collect_all_settings') as collect_mock:
162 162 collect_mock.return_value = expected_result
163 163 result = model.get_repo_settings()
164 164
165 165 collect_mock.assert_called_once_with(global_=False)
166 166 assert result == expected_result
167 167
168 168 @pytest.mark.parametrize('settings, global_', [
169 169 ('global_settings', True),
170 170 ('repo_settings', False)
171 171 ])
172 172 def test_collect_all_settings(self, settings, global_):
173 173 model = VcsSettingsModel()
174 174 result_mock = self._mock_result()
175 175
176 176 settings_patch = mock.patch.object(model, settings)
177 177 with settings_patch as settings_mock:
178 178 settings_mock.get_ui_by_section_and_key.return_value = result_mock
179 179 settings_mock.get_setting_by_name.return_value = result_mock
180 180 result = model._collect_all_settings(global_=global_)
181 181
182 182 ui_settings = model.HG_SETTINGS + model.GIT_SETTINGS + model.HOOKS_SETTINGS
183 183 self._assert_get_settings_calls(
184 184 settings_mock, ui_settings, model.GENERAL_SETTINGS)
185 185 self._assert_collect_all_settings_result(
186 186 ui_settings, model.GENERAL_SETTINGS, result)
187 187
188 188 @pytest.mark.parametrize('settings, global_', [
189 189 ('global_settings', True),
190 190 ('repo_settings', False)
191 191 ])
192 192 def test_collect_all_settings_without_empty_value(self, settings, global_):
193 193 model = VcsSettingsModel()
194 194
195 195 settings_patch = mock.patch.object(model, settings)
196 196 with settings_patch as settings_mock:
197 197 settings_mock.get_ui_by_section_and_key.return_value = None
198 198 settings_mock.get_setting_by_name.return_value = None
199 199 result = model._collect_all_settings(global_=global_)
200 200
201 201 assert result == {}
202 202
203 203 def _mock_result(self):
204 204 result_mock = mock.Mock()
205 205 result_mock.ui_value = 'ui_value'
206 206 result_mock.ui_active = True
207 207 result_mock.app_settings_value = 'setting_value'
208 208 return result_mock
209 209
210 210 def _assert_get_settings_calls(
211 211 self, settings_mock, ui_settings, general_settings):
212 212 assert (
213 213 settings_mock.get_ui_by_section_and_key.call_count ==
214 214 len(ui_settings))
215 215 assert (
216 216 settings_mock.get_setting_by_name.call_count ==
217 217 len(general_settings))
218 218
219 219 for section, key in ui_settings:
220 220 expected_call = mock.call(section, key)
221 221 assert (
222 222 expected_call in
223 223 settings_mock.get_ui_by_section_and_key.call_args_list)
224 224
225 225 for name in general_settings:
226 226 expected_call = mock.call(name)
227 227 assert (
228 228 expected_call in
229 229 settings_mock.get_setting_by_name.call_args_list)
230 230
231 231 def _assert_collect_all_settings_result(
232 232 self, ui_settings, general_settings, result):
233 233 expected_result = {}
234 234 for section, key in ui_settings:
235 235 key = '{}_{}'.format(section, key.replace('.', '_'))
236 236
237 237 if section in ('extensions', 'hooks'):
238 238 value = True
239 239 elif key in ['vcs_git_lfs_enabled']:
240 240 value = True
241 241 else:
242 242 value = 'ui_value'
243 243 expected_result[key] = value
244 244
245 245 for name in general_settings:
246 246 key = 'rhodecode_' + name
247 247 expected_result[key] = 'setting_value'
248 248
249 249 assert expected_result == result
250 250
251 251
252 252 class TestCreateOrUpdateRepoHookSettings(object):
253 253 def test_create_when_no_repo_object_found(self, repo_stub):
254 254 model = VcsSettingsModel(repo=repo_stub.repo_name)
255 255
256 256 self._create_settings(model, HOOKS_FORM_DATA)
257 257
258 258 cleanup = []
259 259 try:
260 260 for section, key in model.HOOKS_SETTINGS:
261 261 ui = model.repo_settings.get_ui_by_section_and_key(
262 262 section, key)
263 263 assert ui.ui_active is True
264 264 cleanup.append(ui)
265 265 finally:
266 266 for ui in cleanup:
267 267 Session().delete(ui)
268 268 Session().commit()
269 269
270 270 def test_create_raises_exception_when_data_incomplete(self, repo_stub):
271 271 model = VcsSettingsModel(repo=repo_stub.repo_name)
272 272
273 273 deleted_key = 'hooks_changegroup_repo_size'
274 274 data = HOOKS_FORM_DATA.copy()
275 275 data.pop(deleted_key)
276 276
277 277 with pytest.raises(ValueError) as exc_info:
278 278 model.create_or_update_repo_hook_settings(data)
279 279 Session().commit()
280 280
281 281 msg = 'The given data does not contain {} key'.format(deleted_key)
282 282 assert str(exc_info.value) == msg
283 283
284 284 def test_update_when_repo_object_found(self, repo_stub, settings_util):
285 285 model = VcsSettingsModel(repo=repo_stub.repo_name)
286 286 for section, key in model.HOOKS_SETTINGS:
287 287 settings_util.create_repo_rhodecode_ui(
288 288 repo_stub, section, None, key=key, active=False)
289 289 model.create_or_update_repo_hook_settings(HOOKS_FORM_DATA)
290 290 Session().commit()
291 291
292 292 for section, key in model.HOOKS_SETTINGS:
293 293 ui = model.repo_settings.get_ui_by_section_and_key(section, key)
294 294 assert ui.ui_active is True
295 295
296 296 def _create_settings(self, model, data):
297 297 global_patch = mock.patch.object(model, 'global_settings')
298 298 global_setting = mock.Mock()
299 299 global_setting.ui_value = 'Test value'
300 300 with global_patch as global_mock:
301 301 global_mock.get_ui_by_section_and_key.return_value = global_setting
302 302 model.create_or_update_repo_hook_settings(HOOKS_FORM_DATA)
303 303 Session().commit()
304 304
305 305
306 306 class TestUpdateGlobalHookSettings(object):
307 307 def test_update_raises_exception_when_data_incomplete(self):
308 308 model = VcsSettingsModel()
309 309
310 310 deleted_key = 'hooks_changegroup_repo_size'
311 311 data = HOOKS_FORM_DATA.copy()
312 312 data.pop(deleted_key)
313 313
314 314 with pytest.raises(ValueError) as exc_info:
315 315 model.update_global_hook_settings(data)
316 316 Session().commit()
317 317
318 318 msg = 'The given data does not contain {} key'.format(deleted_key)
319 319 assert str(exc_info.value) == msg
320 320
321 321 def test_update_global_hook_settings(self, settings_util):
322 322 model = VcsSettingsModel()
323 323 setting_mock = mock.MagicMock()
324 324 setting_mock.ui_active = False
325 325 get_settings_patcher = mock.patch.object(
326 326 model.global_settings, 'get_ui_by_section_and_key',
327 327 return_value=setting_mock)
328 328 session_patcher = mock.patch('rhodecode.model.settings.Session')
329 329 with get_settings_patcher as get_settings_mock, session_patcher:
330 330 model.update_global_hook_settings(HOOKS_FORM_DATA)
331 331 Session().commit()
332 332
333 333 assert setting_mock.ui_active is True
334 334 assert get_settings_mock.call_count == 3
335 335
336 336
337 337 class TestCreateOrUpdateRepoGeneralSettings(object):
338 338 def test_calls_create_or_update_general_settings(self, repo_stub):
339 339 model = VcsSettingsModel(repo=repo_stub.repo_name)
340 340 create_patch = mock.patch.object(
341 341 model, '_create_or_update_general_settings')
342 342 with create_patch as create_mock:
343 343 model.create_or_update_repo_pr_settings(GENERAL_FORM_DATA)
344 344 Session().commit()
345 345
346 346 create_mock.assert_called_once_with(
347 347 model.repo_settings, GENERAL_FORM_DATA)
348 348
349 349 def test_raises_exception_when_repository_is_not_specified(self):
350 350 model = VcsSettingsModel()
351 351 with pytest.raises(Exception) as exc_info:
352 352 model.create_or_update_repo_pr_settings(GENERAL_FORM_DATA)
353 353 assert str(exc_info.value) == 'Repository is not specified'
354 354
355 355
356 356 class TestCreateOrUpdatGlobalGeneralSettings(object):
357 357 def test_calls_create_or_update_general_settings(self):
358 358 model = VcsSettingsModel()
359 359 create_patch = mock.patch.object(
360 360 model, '_create_or_update_general_settings')
361 361 with create_patch as create_mock:
362 362 model.create_or_update_global_pr_settings(GENERAL_FORM_DATA)
363 363 create_mock.assert_called_once_with(
364 364 model.global_settings, GENERAL_FORM_DATA)
365 365
366 366
367 367 class TestCreateOrUpdateGeneralSettings(object):
368 368 def test_create_when_no_repo_settings_found(self, repo_stub):
369 369 model = VcsSettingsModel(repo=repo_stub.repo_name)
370 370 model._create_or_update_general_settings(
371 371 model.repo_settings, GENERAL_FORM_DATA)
372 372
373 373 cleanup = []
374 374 try:
375 375 for name in model.GENERAL_SETTINGS:
376 376 setting = model.repo_settings.get_setting_by_name(name)
377 377 assert setting.app_settings_value is True
378 378 cleanup.append(setting)
379 379 finally:
380 380 for setting in cleanup:
381 381 Session().delete(setting)
382 382 Session().commit()
383 383
384 384 def test_create_raises_exception_when_data_incomplete(self, repo_stub):
385 385 model = VcsSettingsModel(repo=repo_stub.repo_name)
386 386
387 387 deleted_key = 'rhodecode_pr_merge_enabled'
388 388 data = GENERAL_FORM_DATA.copy()
389 389 data.pop(deleted_key)
390 390
391 391 with pytest.raises(ValueError) as exc_info:
392 392 model._create_or_update_general_settings(model.repo_settings, data)
393 393 Session().commit()
394 394
395 395 msg = 'The given data does not contain {} key'.format(deleted_key)
396 396 assert str(exc_info.value) == msg
397 397
398 398 def test_update_when_repo_setting_found(self, repo_stub, settings_util):
399 399 model = VcsSettingsModel(repo=repo_stub.repo_name)
400 400 for name in model.GENERAL_SETTINGS:
401 401 settings_util.create_repo_rhodecode_setting(
402 402 repo_stub, name, False, 'bool')
403 403
404 404 model._create_or_update_general_settings(
405 405 model.repo_settings, GENERAL_FORM_DATA)
406 406 Session().commit()
407 407
408 408 for name in model.GENERAL_SETTINGS:
409 409 setting = model.repo_settings.get_setting_by_name(name)
410 410 assert setting.app_settings_value is True
411 411
412 412
413 413 class TestCreateRepoSvnSettings(object):
414 414 def test_calls_create_svn_settings(self, repo_stub):
415 415 model = VcsSettingsModel(repo=repo_stub.repo_name)
416 416 with mock.patch.object(model, '_create_svn_settings') as create_mock:
417 417 model.create_repo_svn_settings(SVN_FORM_DATA)
418 418 Session().commit()
419 419
420 420 create_mock.assert_called_once_with(model.repo_settings, SVN_FORM_DATA)
421 421
422 422 def test_raises_exception_when_repository_is_not_specified(self):
423 423 model = VcsSettingsModel()
424 424 with pytest.raises(Exception) as exc_info:
425 425 model.create_repo_svn_settings(SVN_FORM_DATA)
426 426 Session().commit()
427 427
428 428 assert str(exc_info.value) == 'Repository is not specified'
429 429
430 430
431 431 class TestCreateSvnSettings(object):
432 432 def test_create(self, repo_stub):
433 433 model = VcsSettingsModel(repo=repo_stub.repo_name)
434 434 model._create_svn_settings(model.repo_settings, SVN_FORM_DATA)
435 435 Session().commit()
436 436
437 437 branch_ui = model.repo_settings.get_ui_by_section(
438 438 model.SVN_BRANCH_SECTION)
439 439 tag_ui = model.repo_settings.get_ui_by_section(
440 440 model.SVN_TAG_SECTION)
441 441
442 442 try:
443 443 assert len(branch_ui) == 1
444 444 assert len(tag_ui) == 1
445 445 finally:
446 446 Session().delete(branch_ui[0])
447 447 Session().delete(tag_ui[0])
448 448 Session().commit()
449 449
450 450 def test_create_tag(self, repo_stub):
451 451 model = VcsSettingsModel(repo=repo_stub.repo_name)
452 452 data = SVN_FORM_DATA.copy()
453 453 data.pop('new_svn_branch')
454 454 model._create_svn_settings(model.repo_settings, data)
455 455 Session().commit()
456 456
457 457 branch_ui = model.repo_settings.get_ui_by_section(
458 458 model.SVN_BRANCH_SECTION)
459 459 tag_ui = model.repo_settings.get_ui_by_section(
460 460 model.SVN_TAG_SECTION)
461 461
462 462 try:
463 463 assert len(branch_ui) == 0
464 464 assert len(tag_ui) == 1
465 465 finally:
466 466 Session().delete(tag_ui[0])
467 467 Session().commit()
468 468
469 469 def test_create_nothing_when_no_svn_settings_specified(self, repo_stub):
470 470 model = VcsSettingsModel(repo=repo_stub.repo_name)
471 471 model._create_svn_settings(model.repo_settings, {})
472 472 Session().commit()
473 473
474 474 branch_ui = model.repo_settings.get_ui_by_section(
475 475 model.SVN_BRANCH_SECTION)
476 476 tag_ui = model.repo_settings.get_ui_by_section(
477 477 model.SVN_TAG_SECTION)
478 478
479 479 assert len(branch_ui) == 0
480 480 assert len(tag_ui) == 0
481 481
482 482 def test_create_nothing_when_empty_settings_specified(self, repo_stub):
483 483 model = VcsSettingsModel(repo=repo_stub.repo_name)
484 484 data = {
485 485 'new_svn_branch': '',
486 486 'new_svn_tag': ''
487 487 }
488 488 model._create_svn_settings(model.repo_settings, data)
489 489 Session().commit()
490 490
491 491 branch_ui = model.repo_settings.get_ui_by_section(
492 492 model.SVN_BRANCH_SECTION)
493 493 tag_ui = model.repo_settings.get_ui_by_section(
494 494 model.SVN_TAG_SECTION)
495 495
496 496 assert len(branch_ui) == 0
497 497 assert len(tag_ui) == 0
498 498
499 499
500 500 class TestCreateOrUpdateUi(object):
501 501 def test_create(self, repo_stub):
502 502 model = VcsSettingsModel(repo=repo_stub.repo_name)
503 503 model._create_or_update_ui(
504 504 model.repo_settings, 'test-section', 'test-key', active=False,
505 505 value='False')
506 506 Session().commit()
507 507
508 508 created_ui = model.repo_settings.get_ui_by_section_and_key(
509 509 'test-section', 'test-key')
510 510
511 511 try:
512 512 assert created_ui.ui_active is False
513 513 assert str2bool(created_ui.ui_value) is False
514 514 finally:
515 515 Session().delete(created_ui)
516 516 Session().commit()
517 517
518 518 def test_update(self, repo_stub, settings_util):
519 519 model = VcsSettingsModel(repo=repo_stub.repo_name)
520 520 # care about only 3 first settings
521 521 largefiles, phases, evolve = model.HG_SETTINGS[:3]
522 522
523 523 section = 'test-section'
524 524 key = 'test-key'
525 525 settings_util.create_repo_rhodecode_ui(
526 526 repo_stub, section, 'True', key=key, active=True)
527 527
528 528 model._create_or_update_ui(
529 529 model.repo_settings, section, key, active=False, value='False')
530 530 Session().commit()
531 531
532 532 created_ui = model.repo_settings.get_ui_by_section_and_key(
533 533 section, key)
534 534 assert created_ui.ui_active is False
535 535 assert str2bool(created_ui.ui_value) is False
536 536
537 537
538 538 class TestCreateOrUpdateRepoHgSettings(object):
539 539 FORM_DATA = {
540 540 'extensions_largefiles': False,
541 541 'extensions_evolve': False,
542 542 'phases_publish': False
543 543 }
544 544
545 545 def test_creates_repo_hg_settings_when_data_is_correct(self, repo_stub):
546 546 model = VcsSettingsModel(repo=repo_stub.repo_name)
547 547 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
548 548 model.create_or_update_repo_hg_settings(self.FORM_DATA)
549 549 expected_calls = [
550 550 mock.call(model.repo_settings, 'extensions', 'largefiles', active=False, value=''),
551 551 mock.call(model.repo_settings, 'extensions', 'evolve', active=False, value=''),
552 552 mock.call(model.repo_settings, 'experimental', 'evolution', active=False, value=''),
553 553 mock.call(model.repo_settings, 'experimental', 'evolution.exchange', active=False, value='no'),
554 554 mock.call(model.repo_settings, 'extensions', 'topic', active=False, value=''),
555 555 mock.call(model.repo_settings, 'phases', 'publish', value='False'),
556 556 ]
557 557 assert expected_calls == create_mock.call_args_list
558 558
559 559 @pytest.mark.parametrize('field_to_remove', FORM_DATA.keys())
560 560 def test_key_is_not_found(self, repo_stub, field_to_remove):
561 561 model = VcsSettingsModel(repo=repo_stub.repo_name)
562 562 data = self.FORM_DATA.copy()
563 563 data.pop(field_to_remove)
564 564 with pytest.raises(ValueError) as exc_info:
565 565 model.create_or_update_repo_hg_settings(data)
566 566 Session().commit()
567 567
568 568 expected_message = 'The given data does not contain {} key'.format(
569 569 field_to_remove)
570 570 assert str(exc_info.value) == expected_message
571 571
572 572 def test_create_raises_exception_when_repository_not_specified(self):
573 573 model = VcsSettingsModel()
574 574 with pytest.raises(Exception) as exc_info:
575 575 model.create_or_update_repo_hg_settings(self.FORM_DATA)
576 576 Session().commit()
577 577
578 578 assert str(exc_info.value) == 'Repository is not specified'
579 579
580 580
581 class TestUpdateGlobalSslSetting(object):
582 def test_updates_global_hg_settings(self):
583 model = VcsSettingsModel()
584 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
585 model.update_global_ssl_setting('False')
586 Session().commit()
587
588 create_mock.assert_called_once_with(
589 model.global_settings, 'web', 'push_ssl', value='False')
590
591
592 581 class TestCreateOrUpdateGlobalHgSettings(object):
593 582 FORM_DATA = {
594 583 'extensions_largefiles': False,
595 584 'phases_publish': False,
596 585 'extensions_evolve': False
597 586 }
598 587
599 588 def test_creates_repo_hg_settings_when_data_is_correct(self):
600 589 model = VcsSettingsModel()
601 590 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
602 591 model.create_or_update_global_hg_settings(self.FORM_DATA)
603 592 Session().commit()
604 593
605 594 expected_calls = [
606 595 mock.call(model.global_settings, 'extensions', 'largefiles', active=False, value=''),
607 596 mock.call(model.global_settings, 'phases', 'publish', value='False'),
608 597 mock.call(model.global_settings, 'extensions', 'evolve', active=False, value=''),
609 598 mock.call(model.global_settings, 'experimental', 'evolution', active=False, value=''),
610 599 mock.call(model.global_settings, 'experimental', 'evolution.exchange', active=False, value='no'),
611 600 mock.call(model.global_settings, 'extensions', 'topic', active=False, value=''),
612 601 ]
613 602
614 603 assert expected_calls == create_mock.call_args_list
615 604
616 605 @pytest.mark.parametrize('field_to_remove', FORM_DATA.keys())
617 606 def test_key_is_not_found(self, repo_stub, field_to_remove):
618 607 model = VcsSettingsModel(repo=repo_stub.repo_name)
619 608 data = self.FORM_DATA.copy()
620 609 data.pop(field_to_remove)
621 610 with pytest.raises(Exception) as exc_info:
622 611 model.create_or_update_global_hg_settings(data)
623 612 Session().commit()
624 613
625 614 expected_message = 'The given data does not contain {} key'.format(
626 615 field_to_remove)
627 616 assert str(exc_info.value) == expected_message
628 617
629 618
630 619 class TestCreateOrUpdateGlobalGitSettings(object):
631 620 FORM_DATA = {
632 621 'vcs_git_lfs_enabled': False,
633 622 }
634 623
635 624 def test_creates_repo_hg_settings_when_data_is_correct(self):
636 625 model = VcsSettingsModel()
637 626 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
638 627 model.create_or_update_global_git_settings(self.FORM_DATA)
639 628 Session().commit()
640 629
641 630 expected_calls = [
642 631 mock.call(model.global_settings, 'vcs_git_lfs', 'enabled', active=False, value=False),
643 632 ]
644 633 assert expected_calls == create_mock.call_args_list
645 634
646 635
647 636 class TestDeleteRepoSvnPattern(object):
648 637 def test_success_when_repo_is_set(self, backend_svn, settings_util):
649 638 repo = backend_svn.create_repo()
650 639 repo_name = repo.repo_name
651 640
652 641 model = VcsSettingsModel(repo=repo_name)
653 642 entry = settings_util.create_repo_rhodecode_ui(
654 643 repo, VcsSettingsModel.SVN_BRANCH_SECTION, 'svn-branch')
655 644 Session().commit()
656 645
657 646 model.delete_repo_svn_pattern(entry.ui_id)
658 647
659 648 def test_fail_when_delete_id_from_other_repo(self, backend_svn):
660 649 repo_name = backend_svn.repo_name
661 650 model = VcsSettingsModel(repo=repo_name)
662 651 delete_ui_patch = mock.patch.object(model.repo_settings, 'delete_ui')
663 652 with delete_ui_patch as delete_ui_mock:
664 653 model.delete_repo_svn_pattern(123)
665 654 Session().commit()
666 655
667 656 delete_ui_mock.assert_called_once_with(-1)
668 657
669 658 def test_raises_exception_when_repository_is_not_specified(self):
670 659 model = VcsSettingsModel()
671 660 with pytest.raises(Exception) as exc_info:
672 661 model.delete_repo_svn_pattern(123)
673 662 assert str(exc_info.value) == 'Repository is not specified'
674 663
675 664
676 665 class TestDeleteGlobalSvnPattern(object):
677 666 def test_delete_global_svn_pattern_calls_delete_ui(self):
678 667 model = VcsSettingsModel()
679 668 delete_ui_patch = mock.patch.object(model.global_settings, 'delete_ui')
680 669 with delete_ui_patch as delete_ui_mock:
681 670 model.delete_global_svn_pattern(123)
682 671 delete_ui_mock.assert_called_once_with(123)
683 672
684 673
685 674 class TestFilterUiSettings(object):
686 675 def test_settings_are_filtered(self):
687 676 model = VcsSettingsModel()
688 677 repo_settings = [
689 678 UiSetting('extensions', 'largefiles', '', True),
690 679 UiSetting('phases', 'publish', 'True', True),
691 680 UiSetting('hooks', 'changegroup.repo_size', 'hook', True),
692 681 UiSetting('hooks', 'changegroup.push_logger', 'hook', True),
693 682 UiSetting('hooks', 'outgoing.pull_logger', 'hook', True),
694 683 UiSetting(
695 684 'vcs_svn_branch', '84223c972204fa545ca1b22dac7bef5b68d7442d',
696 685 'test_branch', True),
697 686 UiSetting(
698 687 'vcs_svn_tag', '84229c972204fa545ca1b22dac7bef5b68d7442d',
699 688 'test_tag', True),
700 689 ]
701 690 non_repo_settings = [
702 691 UiSetting('largefiles', 'usercache', '/example/largefiles-store', True),
703 692 UiSetting('test', 'outgoing.pull_logger', 'hook', True),
704 693 UiSetting('hooks', 'test2', 'hook', True),
705 694 UiSetting(
706 695 'vcs_svn_repo', '84229c972204fa545ca1b22dac7bef5b68d7442d',
707 696 'test_tag', True),
708 697 ]
709 698 settings = repo_settings + non_repo_settings
710 699 filtered_settings = model._filter_ui_settings(settings)
711 700 assert sorted(filtered_settings) == sorted(repo_settings)
712 701
713 702
714 703 class TestFilterGeneralSettings(object):
715 704 def test_settings_are_filtered(self):
716 705 model = VcsSettingsModel()
717 706 settings = {
718 707 'rhodecode_abcde': 'value1',
719 708 'rhodecode_vwxyz': 'value2',
720 709 }
721 710 general_settings = {
722 711 'rhodecode_{}'.format(key): 'value'
723 712 for key in VcsSettingsModel.GENERAL_SETTINGS
724 713 }
725 714 settings.update(general_settings)
726 715
727 716 filtered_settings = model._filter_general_settings(general_settings)
728 717 assert sorted(filtered_settings) == sorted(general_settings)
729 718
730 719
731 720 class TestGetRepoUiSettings(object):
732 721 def test_global_uis_are_returned_when_no_repo_uis_found(
733 722 self, repo_stub):
734 723 model = VcsSettingsModel(repo=repo_stub.repo_name)
735 724 result = model.get_repo_ui_settings()
736 725 svn_sections = (
737 726 VcsSettingsModel.SVN_TAG_SECTION,
738 727 VcsSettingsModel.SVN_BRANCH_SECTION)
739 728 expected_result = [
740 729 s for s in model.global_settings.get_ui()
741 730 if s.section not in svn_sections]
742 731 assert sorted(result) == sorted(expected_result)
743 732
744 733 def test_repo_uis_are_overriding_global_uis(
745 734 self, repo_stub, settings_util):
746 735 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
747 736 settings_util.create_repo_rhodecode_ui(
748 737 repo_stub, section, 'repo', key=key, active=False)
749 738 model = VcsSettingsModel(repo=repo_stub.repo_name)
750 739 result = model.get_repo_ui_settings()
751 740 for setting in result:
752 741 locator = (setting.section, setting.key)
753 742 if locator in VcsSettingsModel.HOOKS_SETTINGS:
754 743 assert setting.value == 'repo'
755 744
756 745 assert setting.active is False
757 746
758 747 def test_global_svn_patterns_are_not_in_list(
759 748 self, repo_stub, settings_util):
760 749 svn_sections = (
761 750 VcsSettingsModel.SVN_TAG_SECTION,
762 751 VcsSettingsModel.SVN_BRANCH_SECTION)
763 752 for section in svn_sections:
764 753 settings_util.create_rhodecode_ui(
765 754 section, 'repo', key='deadbeef' + section, active=False)
766 755 Session().commit()
767 756
768 757 model = VcsSettingsModel(repo=repo_stub.repo_name)
769 758 result = model.get_repo_ui_settings()
770 759 for setting in result:
771 760 assert setting.section not in svn_sections
772 761
773 762 def test_repo_uis_filtered_by_section_are_returned(
774 763 self, repo_stub, settings_util):
775 764 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
776 765 settings_util.create_repo_rhodecode_ui(
777 766 repo_stub, section, 'repo', key=key, active=False)
778 767 model = VcsSettingsModel(repo=repo_stub.repo_name)
779 768 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
780 769 result = model.get_repo_ui_settings(section=section)
781 770 for setting in result:
782 771 assert setting.section == section
783 772
784 773 def test_repo_uis_filtered_by_key_are_returned(
785 774 self, repo_stub, settings_util):
786 775 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
787 776 settings_util.create_repo_rhodecode_ui(
788 777 repo_stub, section, 'repo', key=key, active=False)
789 778 model = VcsSettingsModel(repo=repo_stub.repo_name)
790 779 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
791 780 result = model.get_repo_ui_settings(key=key)
792 781 for setting in result:
793 782 assert setting.key == key
794 783
795 784 def test_raises_exception_when_repository_is_not_specified(self):
796 785 model = VcsSettingsModel()
797 786 with pytest.raises(Exception) as exc_info:
798 787 model.get_repo_ui_settings()
799 788 assert str(exc_info.value) == 'Repository is not specified'
800 789
801 790
802 791 class TestGetRepoGeneralSettings(object):
803 792 def test_global_settings_are_returned_when_no_repo_settings_found(
804 793 self, repo_stub):
805 794 model = VcsSettingsModel(repo=repo_stub.repo_name)
806 795 result = model.get_repo_general_settings()
807 796 expected_result = model.global_settings.get_all_settings()
808 797 assert sorted(result) == sorted(expected_result)
809 798
810 799 def test_repo_uis_are_overriding_global_uis(
811 800 self, repo_stub, settings_util):
812 801 for key in VcsSettingsModel.GENERAL_SETTINGS:
813 802 settings_util.create_repo_rhodecode_setting(
814 803 repo_stub, key, 'abcde', type_='unicode')
815 804 Session().commit()
816 805
817 806 model = VcsSettingsModel(repo=repo_stub.repo_name)
818 807 result = model.get_repo_ui_settings()
819 808 for key in result:
820 809 if key in VcsSettingsModel.GENERAL_SETTINGS:
821 810 assert result[key] == 'abcde'
822 811
823 812 def test_raises_exception_when_repository_is_not_specified(self):
824 813 model = VcsSettingsModel()
825 814 with pytest.raises(Exception) as exc_info:
826 815 model.get_repo_general_settings()
827 816 assert str(exc_info.value) == 'Repository is not specified'
828 817
829 818
830 819 class TestGetGlobalGeneralSettings(object):
831 820 def test_global_settings_are_returned(self, repo_stub):
832 821 model = VcsSettingsModel()
833 822 result = model.get_global_general_settings()
834 823 expected_result = model.global_settings.get_all_settings()
835 824 assert sorted(result) == sorted(expected_result)
836 825
837 826 def test_repo_uis_are_not_overriding_global_uis(
838 827 self, repo_stub, settings_util):
839 828 for key in VcsSettingsModel.GENERAL_SETTINGS:
840 829 settings_util.create_repo_rhodecode_setting(
841 830 repo_stub, key, 'abcde', type_='unicode')
842 831 Session().commit()
843 832
844 833 model = VcsSettingsModel(repo=repo_stub.repo_name)
845 834 result = model.get_global_general_settings()
846 835 expected_result = model.global_settings.get_all_settings()
847 836 assert sorted(result) == sorted(expected_result)
848 837
849 838
850 839 class TestGetGlobalUiSettings(object):
851 840 def test_global_uis_are_returned(self, repo_stub):
852 841 model = VcsSettingsModel()
853 842 result = model.get_global_ui_settings()
854 843 expected_result = model.global_settings.get_ui()
855 844 assert sorted(result) == sorted(expected_result)
856 845
857 846 def test_repo_uis_are_not_overriding_global_uis(
858 847 self, repo_stub, settings_util):
859 848 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
860 849 settings_util.create_repo_rhodecode_ui(
861 850 repo_stub, section, 'repo', key=key, active=False)
862 851 Session().commit()
863 852
864 853 model = VcsSettingsModel(repo=repo_stub.repo_name)
865 854 result = model.get_global_ui_settings()
866 855 expected_result = model.global_settings.get_ui()
867 856 assert sorted(result) == sorted(expected_result)
868 857
869 858 def test_ui_settings_filtered_by_section(
870 859 self, repo_stub, settings_util):
871 860 model = VcsSettingsModel(repo=repo_stub.repo_name)
872 861 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
873 862 result = model.get_global_ui_settings(section=section)
874 863 expected_result = model.global_settings.get_ui(section=section)
875 864 assert sorted(result) == sorted(expected_result)
876 865
877 866 def test_ui_settings_filtered_by_key(
878 867 self, repo_stub, settings_util):
879 868 model = VcsSettingsModel(repo=repo_stub.repo_name)
880 869 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
881 870 result = model.get_global_ui_settings(key=key)
882 871 expected_result = model.global_settings.get_ui(key=key)
883 872 assert sorted(result) == sorted(expected_result)
884 873
885 874
886 875 class TestGetGeneralSettings(object):
887 876 def test_global_settings_are_returned_when_inherited_is_true(
888 877 self, repo_stub, settings_util):
889 878 model = VcsSettingsModel(repo=repo_stub.repo_name)
890 879 model.inherit_global_settings = True
891 880 for key in VcsSettingsModel.GENERAL_SETTINGS:
892 881 settings_util.create_repo_rhodecode_setting(
893 882 repo_stub, key, 'abcde', type_='unicode')
894 883 Session().commit()
895 884
896 885 result = model.get_general_settings()
897 886 expected_result = model.get_global_general_settings()
898 887 assert sorted(result) == sorted(expected_result)
899 888
900 889 def test_repo_settings_are_returned_when_inherited_is_false(
901 890 self, repo_stub, settings_util):
902 891 model = VcsSettingsModel(repo=repo_stub.repo_name)
903 892 model.inherit_global_settings = False
904 893 for key in VcsSettingsModel.GENERAL_SETTINGS:
905 894 settings_util.create_repo_rhodecode_setting(
906 895 repo_stub, key, 'abcde', type_='unicode')
907 896 Session().commit()
908 897
909 898 result = model.get_general_settings()
910 899 expected_result = model.get_repo_general_settings()
911 900 assert sorted(result) == sorted(expected_result)
912 901
913 902 def test_global_settings_are_returned_when_no_repository_specified(self):
914 903 model = VcsSettingsModel()
915 904 result = model.get_general_settings()
916 905 expected_result = model.get_global_general_settings()
917 906 assert sorted(result) == sorted(expected_result)
918 907
919 908
920 909 class TestGetUiSettings(object):
921 910 def test_global_settings_are_returned_when_inherited_is_true(
922 911 self, repo_stub, settings_util):
923 912 model = VcsSettingsModel(repo=repo_stub.repo_name)
924 913 model.inherit_global_settings = True
925 914 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
926 915 settings_util.create_repo_rhodecode_ui(
927 916 repo_stub, section, 'repo', key=key, active=True)
928 917 Session().commit()
929 918
930 919 result = model.get_ui_settings()
931 920 expected_result = model.get_global_ui_settings()
932 921 assert sorted(result) == sorted(expected_result)
933 922
934 923 def test_repo_settings_are_returned_when_inherited_is_false(
935 924 self, repo_stub, settings_util):
936 925 model = VcsSettingsModel(repo=repo_stub.repo_name)
937 926 model.inherit_global_settings = False
938 927 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
939 928 settings_util.create_repo_rhodecode_ui(
940 929 repo_stub, section, 'repo', key=key, active=True)
941 930 Session().commit()
942 931
943 932 result = model.get_ui_settings()
944 933 expected_result = model.get_repo_ui_settings()
945 934 assert sorted(result) == sorted(expected_result)
946 935
947 936 def test_repo_settings_filtered_by_section_and_key(self, repo_stub):
948 937 model = VcsSettingsModel(repo=repo_stub.repo_name)
949 938 model.inherit_global_settings = False
950 939
951 940 args = ('section', 'key')
952 941 with mock.patch.object(model, 'get_repo_ui_settings') as settings_mock:
953 942 model.get_ui_settings(*args)
954 943 Session().commit()
955 944
956 945 settings_mock.assert_called_once_with(*args)
957 946
958 947 def test_global_settings_filtered_by_section_and_key(self):
959 948 model = VcsSettingsModel()
960 949 args = ('section', 'key')
961 950 with mock.patch.object(model, 'get_global_ui_settings') as (
962 951 settings_mock):
963 952 model.get_ui_settings(*args)
964 953 settings_mock.assert_called_once_with(*args)
965 954
966 955 def test_global_settings_are_returned_when_no_repository_specified(self):
967 956 model = VcsSettingsModel()
968 957 result = model.get_ui_settings()
969 958 expected_result = model.get_global_ui_settings()
970 959 assert sorted(result) == sorted(expected_result)
971 960
972 961
973 962 class TestGetSvnPatterns(object):
974 963 def test_repo_settings_filtered_by_section_and_key(self, repo_stub):
975 964 model = VcsSettingsModel(repo=repo_stub.repo_name)
976 965 args = ('section', )
977 966 with mock.patch.object(model, 'get_repo_ui_settings') as settings_mock:
978 967 model.get_svn_patterns(*args)
979 968
980 969 Session().commit()
981 970 settings_mock.assert_called_once_with(*args)
982 971
983 972 def test_global_settings_filtered_by_section_and_key(self):
984 973 model = VcsSettingsModel()
985 974 args = ('section', )
986 975 with mock.patch.object(model, 'get_global_ui_settings') as (
987 976 settings_mock):
988 977 model.get_svn_patterns(*args)
989 978 settings_mock.assert_called_once_with(*args)
990 979
991 980
992 981 class TestCreateOrUpdateRepoSettings(object):
993 982 FORM_DATA = {
994 983 'inherit_global_settings': False,
995 984 'hooks_changegroup_repo_size': False,
996 985 'hooks_changegroup_push_logger': False,
997 986 'hooks_outgoing_pull_logger': False,
998 987 'extensions_largefiles': False,
999 988 'extensions_evolve': False,
1000 989 'vcs_git_lfs_enabled': False,
1001 990 'phases_publish': 'False',
1002 991 'rhodecode_pr_merge_enabled': False,
1003 992 'rhodecode_use_outdated_comments': False,
1004 993 'new_svn_branch': '',
1005 994 'new_svn_tag': ''
1006 995 }
1007 996
1008 997 def test_get_raises_exception_when_repository_not_specified(self):
1009 998 model = VcsSettingsModel()
1010 999 with pytest.raises(Exception) as exc_info:
1011 1000 model.create_or_update_repo_settings(data=self.FORM_DATA)
1012 1001 Session().commit()
1013 1002
1014 1003 assert str(exc_info.value) == 'Repository is not specified'
1015 1004
1016 1005 def test_only_svn_settings_are_updated_when_type_is_svn(self, backend_svn):
1017 1006 repo = backend_svn.create_repo()
1018 1007 model = VcsSettingsModel(repo=repo)
1019 1008 with self._patch_model(model) as mocks:
1020 1009 model.create_or_update_repo_settings(
1021 1010 data=self.FORM_DATA, inherit_global_settings=False)
1022 1011 Session().commit()
1023 1012
1024 1013 mocks['create_repo_svn_settings'].assert_called_once_with(
1025 1014 self.FORM_DATA)
1026 1015 non_called_methods = (
1027 1016 'create_or_update_repo_hook_settings',
1028 1017 'create_or_update_repo_pr_settings',
1029 1018 'create_or_update_repo_hg_settings')
1030 1019 for method in non_called_methods:
1031 1020 assert mocks[method].call_count == 0
1032 1021
1033 1022 def test_non_svn_settings_are_updated_when_type_is_hg(self, backend_hg):
1034 1023 repo = backend_hg.create_repo()
1035 1024 model = VcsSettingsModel(repo=repo)
1036 1025 with self._patch_model(model) as mocks:
1037 1026 model.create_or_update_repo_settings(
1038 1027 data=self.FORM_DATA, inherit_global_settings=False)
1039 1028 Session().commit()
1040 1029
1041 1030 assert mocks['create_repo_svn_settings'].call_count == 0
1042 1031 called_methods = (
1043 1032 'create_or_update_repo_hook_settings',
1044 1033 'create_or_update_repo_pr_settings',
1045 1034 'create_or_update_repo_hg_settings')
1046 1035 for method in called_methods:
1047 1036 mocks[method].assert_called_once_with(self.FORM_DATA)
1048 1037
1049 1038 def test_non_svn_and_hg_settings_are_updated_when_type_is_git(
1050 1039 self, backend_git):
1051 1040 repo = backend_git.create_repo()
1052 1041 model = VcsSettingsModel(repo=repo)
1053 1042 with self._patch_model(model) as mocks:
1054 1043 model.create_or_update_repo_settings(
1055 1044 data=self.FORM_DATA, inherit_global_settings=False)
1056 1045
1057 1046 assert mocks['create_repo_svn_settings'].call_count == 0
1058 1047 called_methods = (
1059 1048 'create_or_update_repo_hook_settings',
1060 1049 'create_or_update_repo_pr_settings')
1061 1050 non_called_methods = (
1062 1051 'create_repo_svn_settings',
1063 1052 'create_or_update_repo_hg_settings'
1064 1053 )
1065 1054 for method in called_methods:
1066 1055 mocks[method].assert_called_once_with(self.FORM_DATA)
1067 1056 for method in non_called_methods:
1068 1057 assert mocks[method].call_count == 0
1069 1058
1070 1059 def test_no_methods_are_called_when_settings_are_inherited(
1071 1060 self, backend):
1072 1061 repo = backend.create_repo()
1073 1062 model = VcsSettingsModel(repo=repo)
1074 1063 with self._patch_model(model) as mocks:
1075 1064 model.create_or_update_repo_settings(
1076 1065 data=self.FORM_DATA, inherit_global_settings=True)
1077 1066 for method_name in mocks:
1078 1067 assert mocks[method_name].call_count == 0
1079 1068
1080 1069 def test_cache_is_marked_for_invalidation(self, repo_stub):
1081 1070 model = VcsSettingsModel(repo=repo_stub)
1082 1071 invalidation_patcher = mock.patch(
1083 1072 'rhodecode.model.scm.ScmModel.mark_for_invalidation')
1084 1073 with invalidation_patcher as invalidation_mock:
1085 1074 model.create_or_update_repo_settings(
1086 1075 data=self.FORM_DATA, inherit_global_settings=True)
1087 1076 Session().commit()
1088 1077
1089 1078 invalidation_mock.assert_called_once_with(
1090 1079 repo_stub.repo_name, delete=True)
1091 1080
1092 1081 def test_inherit_flag_is_saved(self, repo_stub):
1093 1082 model = VcsSettingsModel(repo=repo_stub)
1094 1083 model.inherit_global_settings = True
1095 1084 with self._patch_model(model):
1096 1085 model.create_or_update_repo_settings(
1097 1086 data=self.FORM_DATA, inherit_global_settings=False)
1098 1087 Session().commit()
1099 1088
1100 1089 assert model.inherit_global_settings is False
1101 1090
1102 1091 def _patch_model(self, model):
1103 1092 return mock.patch.multiple(
1104 1093 model,
1105 1094 create_repo_svn_settings=mock.DEFAULT,
1106 1095 create_or_update_repo_hook_settings=mock.DEFAULT,
1107 1096 create_or_update_repo_pr_settings=mock.DEFAULT,
1108 1097 create_or_update_repo_hg_settings=mock.DEFAULT)
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now