##// END OF EJS Templates
api: security, fix problem when absolute paths are specified with API call, that would allow...
marcink -
r2664:36dbf06f stable
parent child Browse files
Show More
@@ -1,192 +1,194 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import mock
22 22 import pytest
23 23
24 24 from rhodecode.model.repo import RepoModel
25 25 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN
26 26 from rhodecode.api.tests.utils import (
27 27 build_data, api_call, assert_error, assert_ok, crash, jsonify)
28 28 from rhodecode.tests.fixture import Fixture
29 29 from rhodecode.tests.plugin import http_host_stub, http_host_only_stub
30 30
31 31 fixture = Fixture()
32 32
33 33 UPDATE_REPO_NAME = 'api_update_me'
34 34
35 35
36 36 class SAME_AS_UPDATES(object):
37 37 """ Constant used for tests below """
38 38
39 39
40 40 @pytest.mark.usefixtures("testuser_api", "app")
41 41 class TestApiUpdateRepo(object):
42 42
43 43 @pytest.mark.parametrize("updates, expected", [
44 44 ({'owner': TEST_USER_REGULAR_LOGIN},
45 45 SAME_AS_UPDATES),
46 46
47 47 ({'description': 'new description'},
48 48 SAME_AS_UPDATES),
49 49
50 50 ({'clone_uri': 'http://foo.com/repo'},
51 51 SAME_AS_UPDATES),
52 52
53 53 ({'clone_uri': None},
54 54 {'clone_uri': ''}),
55 55
56 56 ({'clone_uri': ''},
57 57 {'clone_uri': ''}),
58 58
59 59 ({'landing_rev': 'rev:tip'},
60 60 {'landing_rev': ['rev', 'tip']}),
61 61
62 62 ({'enable_statistics': True},
63 63 SAME_AS_UPDATES),
64 64
65 65 ({'enable_locking': True},
66 66 SAME_AS_UPDATES),
67 67
68 68 ({'enable_downloads': True},
69 69 SAME_AS_UPDATES),
70 70
71 71 ({'repo_name': 'new_repo_name'},
72 72 {
73 73 'repo_name': 'new_repo_name',
74 74 'url': 'http://{}/new_repo_name'.format(http_host_only_stub())
75 75 }),
76 76
77 77 ({'repo_name': 'test_group_for_update/{}'.format(UPDATE_REPO_NAME),
78 78 '_group': 'test_group_for_update'},
79 79 {
80 80 'repo_name': 'test_group_for_update/{}'.format(UPDATE_REPO_NAME),
81 81 'url': 'http://{}/test_group_for_update/{}'.format(
82 82 http_host_only_stub(), UPDATE_REPO_NAME)
83 83 }),
84 84 ])
85 85 def test_api_update_repo(self, updates, expected, backend):
86 86 repo_name = UPDATE_REPO_NAME
87 87 repo = fixture.create_repo(repo_name, repo_type=backend.alias)
88 88 if updates.get('_group'):
89 89 fixture.create_repo_group(updates['_group'])
90 90
91 91 expected_api_data = repo.get_api_data(include_secrets=True)
92 92 if expected is SAME_AS_UPDATES:
93 93 expected_api_data.update(updates)
94 94 else:
95 95 expected_api_data.update(expected)
96 96
97 97 id_, params = build_data(
98 98 self.apikey, 'update_repo', repoid=repo_name, **updates)
99
100 with mock.patch('rhodecode.model.validation_schema.validators.url_validator'):
99 101 response = api_call(self.app, params)
100 102
101 103 if updates.get('repo_name'):
102 104 repo_name = updates['repo_name']
103 105
104 106 try:
105 107 expected = {
106 108 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo_name),
107 109 'repository': jsonify(expected_api_data)
108 110 }
109 111 assert_ok(id_, expected, given=response.body)
110 112 finally:
111 113 fixture.destroy_repo(repo_name)
112 114 if updates.get('_group'):
113 115 fixture.destroy_repo_group(updates['_group'])
114 116
115 117 def test_api_update_repo_fork_of_field(self, backend):
116 118 master_repo = backend.create_repo()
117 119 repo = backend.create_repo()
118 120 updates = {
119 121 'fork_of': master_repo.repo_name,
120 122 'fork_of_id': master_repo.repo_id
121 123 }
122 124 expected_api_data = repo.get_api_data(include_secrets=True)
123 125 expected_api_data.update(updates)
124 126
125 127 id_, params = build_data(
126 128 self.apikey, 'update_repo', repoid=repo.repo_name, **updates)
127 129 response = api_call(self.app, params)
128 130 expected = {
129 131 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
130 132 'repository': jsonify(expected_api_data)
131 133 }
132 134 assert_ok(id_, expected, given=response.body)
133 135 result = response.json['result']['repository']
134 136 assert result['fork_of'] == master_repo.repo_name
135 137 assert result['fork_of_id'] == master_repo.repo_id
136 138
137 139 def test_api_update_repo_fork_of_not_found(self, backend):
138 140 master_repo_name = 'fake-parent-repo'
139 141 repo = backend.create_repo()
140 142 updates = {
141 143 'fork_of': master_repo_name
142 144 }
143 145 id_, params = build_data(
144 146 self.apikey, 'update_repo', repoid=repo.repo_name, **updates)
145 147 response = api_call(self.app, params)
146 148 expected = {
147 149 'repo_fork_of': 'Fork with id `{}` does not exists'.format(
148 150 master_repo_name)}
149 151 assert_error(id_, expected, given=response.body)
150 152
151 153 def test_api_update_repo_with_repo_group_not_existing(self):
152 154 repo_name = 'admin_owned'
153 155 fake_repo_group = 'test_group_for_update'
154 156 fixture.create_repo(repo_name)
155 157 updates = {'repo_name': '{}/{}'.format(fake_repo_group, repo_name)}
156 158 id_, params = build_data(
157 159 self.apikey, 'update_repo', repoid=repo_name, **updates)
158 160 response = api_call(self.app, params)
159 161 try:
160 162 expected = {
161 163 'repo_group': 'Repository group `{}` does not exist'.format(fake_repo_group)
162 164 }
163 165 assert_error(id_, expected, given=response.body)
164 166 finally:
165 167 fixture.destroy_repo(repo_name)
166 168
167 169 def test_api_update_repo_regular_user_not_allowed(self):
168 170 repo_name = 'admin_owned'
169 171 fixture.create_repo(repo_name)
170 172 updates = {'active': False}
171 173 id_, params = build_data(
172 174 self.apikey_regular, 'update_repo', repoid=repo_name, **updates)
173 175 response = api_call(self.app, params)
174 176 try:
175 177 expected = 'repository `%s` does not exist' % (repo_name,)
176 178 assert_error(id_, expected, given=response.body)
177 179 finally:
178 180 fixture.destroy_repo(repo_name)
179 181
180 182 @mock.patch.object(RepoModel, 'update', crash)
181 183 def test_api_update_repo_exception_occurred(self, backend):
182 184 repo_name = UPDATE_REPO_NAME
183 185 fixture.create_repo(repo_name, repo_type=backend.alias)
184 186 id_, params = build_data(
185 187 self.apikey, 'update_repo', repoid=repo_name,
186 188 owner=TEST_USER_ADMIN_LOGIN,)
187 189 response = api_call(self.app, params)
188 190 try:
189 191 expected = 'failed to update repo `%s`' % (repo_name,)
190 192 assert_error(id_, expected, given=response.body)
191 193 finally:
192 194 fixture.destroy_repo(repo_name)
@@ -1,2042 +1,2046 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import time
23 23
24 24 import rhodecode
25 25 from rhodecode.api import (
26 26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
27 27 from rhodecode.api.utils import (
28 28 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
29 29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
30 30 get_perm_or_error, parse_args, get_origin, build_commit_data,
31 31 validate_set_owner_permissions)
32 32 from rhodecode.lib import audit_logger
33 33 from rhodecode.lib import repo_maintenance
34 34 from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi
35 35 from rhodecode.lib.celerylib.utils import get_task_id
36 36 from rhodecode.lib.utils2 import str2bool, time_to_datetime
37 37 from rhodecode.lib.ext_json import json
38 38 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
39 39 from rhodecode.model.changeset_status import ChangesetStatusModel
40 40 from rhodecode.model.comment import CommentsModel
41 41 from rhodecode.model.db import (
42 42 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
43 43 ChangesetComment)
44 44 from rhodecode.model.repo import RepoModel
45 45 from rhodecode.model.scm import ScmModel, RepoList
46 46 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
47 47 from rhodecode.model import validation_schema
48 48 from rhodecode.model.validation_schema.schemas import repo_schema
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52
53 53 @jsonrpc_method()
54 54 def get_repo(request, apiuser, repoid, cache=Optional(True)):
55 55 """
56 56 Gets an existing repository by its name or repository_id.
57 57
58 58 The members section so the output returns users groups or users
59 59 associated with that repository.
60 60
61 61 This command can only be run using an |authtoken| with admin rights,
62 62 or users with at least read rights to the |repo|.
63 63
64 64 :param apiuser: This is filled automatically from the |authtoken|.
65 65 :type apiuser: AuthUser
66 66 :param repoid: The repository name or repository id.
67 67 :type repoid: str or int
68 68 :param cache: use the cached value for last changeset
69 69 :type: cache: Optional(bool)
70 70
71 71 Example output:
72 72
73 73 .. code-block:: bash
74 74
75 75 {
76 76 "error": null,
77 77 "id": <repo_id>,
78 78 "result": {
79 79 "clone_uri": null,
80 80 "created_on": "timestamp",
81 81 "description": "repo description",
82 82 "enable_downloads": false,
83 83 "enable_locking": false,
84 84 "enable_statistics": false,
85 85 "followers": [
86 86 {
87 87 "active": true,
88 88 "admin": false,
89 89 "api_key": "****************************************",
90 90 "api_keys": [
91 91 "****************************************"
92 92 ],
93 93 "email": "user@example.com",
94 94 "emails": [
95 95 "user@example.com"
96 96 ],
97 97 "extern_name": "rhodecode",
98 98 "extern_type": "rhodecode",
99 99 "firstname": "username",
100 100 "ip_addresses": [],
101 101 "language": null,
102 102 "last_login": "2015-09-16T17:16:35.854",
103 103 "lastname": "surname",
104 104 "user_id": <user_id>,
105 105 "username": "name"
106 106 }
107 107 ],
108 108 "fork_of": "parent-repo",
109 109 "landing_rev": [
110 110 "rev",
111 111 "tip"
112 112 ],
113 113 "last_changeset": {
114 114 "author": "User <user@example.com>",
115 115 "branch": "default",
116 116 "date": "timestamp",
117 117 "message": "last commit message",
118 118 "parents": [
119 119 {
120 120 "raw_id": "commit-id"
121 121 }
122 122 ],
123 123 "raw_id": "commit-id",
124 124 "revision": <revision number>,
125 125 "short_id": "short id"
126 126 },
127 127 "lock_reason": null,
128 128 "locked_by": null,
129 129 "locked_date": null,
130 130 "owner": "owner-name",
131 131 "permissions": [
132 132 {
133 133 "name": "super-admin-name",
134 134 "origin": "super-admin",
135 135 "permission": "repository.admin",
136 136 "type": "user"
137 137 },
138 138 {
139 139 "name": "owner-name",
140 140 "origin": "owner",
141 141 "permission": "repository.admin",
142 142 "type": "user"
143 143 },
144 144 {
145 145 "name": "user-group-name",
146 146 "origin": "permission",
147 147 "permission": "repository.write",
148 148 "type": "user_group"
149 149 }
150 150 ],
151 151 "private": true,
152 152 "repo_id": 676,
153 153 "repo_name": "user-group/repo-name",
154 154 "repo_type": "hg"
155 155 }
156 156 }
157 157 """
158 158
159 159 repo = get_repo_or_error(repoid)
160 160 cache = Optional.extract(cache)
161 161
162 162 include_secrets = False
163 163 if has_superadmin_permission(apiuser):
164 164 include_secrets = True
165 165 else:
166 166 # check if we have at least read permission for this repo !
167 167 _perms = (
168 168 'repository.admin', 'repository.write', 'repository.read',)
169 169 validate_repo_permissions(apiuser, repoid, repo, _perms)
170 170
171 171 permissions = []
172 172 for _user in repo.permissions():
173 173 user_data = {
174 174 'name': _user.username,
175 175 'permission': _user.permission,
176 176 'origin': get_origin(_user),
177 177 'type': "user",
178 178 }
179 179 permissions.append(user_data)
180 180
181 181 for _user_group in repo.permission_user_groups():
182 182 user_group_data = {
183 183 'name': _user_group.users_group_name,
184 184 'permission': _user_group.permission,
185 185 'origin': get_origin(_user_group),
186 186 'type': "user_group",
187 187 }
188 188 permissions.append(user_group_data)
189 189
190 190 following_users = [
191 191 user.user.get_api_data(include_secrets=include_secrets)
192 192 for user in repo.followers]
193 193
194 194 if not cache:
195 195 repo.update_commit_cache()
196 196 data = repo.get_api_data(include_secrets=include_secrets)
197 197 data['permissions'] = permissions
198 198 data['followers'] = following_users
199 199 return data
200 200
201 201
202 202 @jsonrpc_method()
203 203 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
204 204 """
205 205 Lists all existing repositories.
206 206
207 207 This command can only be run using an |authtoken| with admin rights,
208 208 or users with at least read rights to |repos|.
209 209
210 210 :param apiuser: This is filled automatically from the |authtoken|.
211 211 :type apiuser: AuthUser
212 212 :param root: specify root repository group to fetch repositories.
213 213 filters the returned repositories to be members of given root group.
214 214 :type root: Optional(None)
215 215 :param traverse: traverse given root into subrepositories. With this flag
216 216 set to False, it will only return top-level repositories from `root`.
217 217 if root is empty it will return just top-level repositories.
218 218 :type traverse: Optional(True)
219 219
220 220
221 221 Example output:
222 222
223 223 .. code-block:: bash
224 224
225 225 id : <id_given_in_input>
226 226 result: [
227 227 {
228 228 "repo_id" : "<repo_id>",
229 229 "repo_name" : "<reponame>"
230 230 "repo_type" : "<repo_type>",
231 231 "clone_uri" : "<clone_uri>",
232 232 "private": : "<bool>",
233 233 "created_on" : "<datetimecreated>",
234 234 "description" : "<description>",
235 235 "landing_rev": "<landing_rev>",
236 236 "owner": "<repo_owner>",
237 237 "fork_of": "<name_of_fork_parent>",
238 238 "enable_downloads": "<bool>",
239 239 "enable_locking": "<bool>",
240 240 "enable_statistics": "<bool>",
241 241 },
242 242 ...
243 243 ]
244 244 error: null
245 245 """
246 246
247 247 include_secrets = has_superadmin_permission(apiuser)
248 248 _perms = ('repository.read', 'repository.write', 'repository.admin',)
249 249 extras = {'user': apiuser}
250 250
251 251 root = Optional.extract(root)
252 252 traverse = Optional.extract(traverse, binary=True)
253 253
254 254 if root:
255 255 # verify parent existance, if it's empty return an error
256 256 parent = RepoGroup.get_by_group_name(root)
257 257 if not parent:
258 258 raise JSONRPCError(
259 259 'Root repository group `{}` does not exist'.format(root))
260 260
261 261 if traverse:
262 262 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
263 263 else:
264 264 repos = RepoModel().get_repos_for_root(root=parent)
265 265 else:
266 266 if traverse:
267 267 repos = RepoModel().get_all()
268 268 else:
269 269 # return just top-level
270 270 repos = RepoModel().get_repos_for_root(root=None)
271 271
272 272 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
273 273 return [repo.get_api_data(include_secrets=include_secrets)
274 274 for repo in repo_list]
275 275
276 276
277 277 @jsonrpc_method()
278 278 def get_repo_changeset(request, apiuser, repoid, revision,
279 279 details=Optional('basic')):
280 280 """
281 281 Returns information about a changeset.
282 282
283 283 Additionally parameters define the amount of details returned by
284 284 this function.
285 285
286 286 This command can only be run using an |authtoken| with admin rights,
287 287 or users with at least read rights to the |repo|.
288 288
289 289 :param apiuser: This is filled automatically from the |authtoken|.
290 290 :type apiuser: AuthUser
291 291 :param repoid: The repository name or repository id
292 292 :type repoid: str or int
293 293 :param revision: revision for which listing should be done
294 294 :type revision: str
295 295 :param details: details can be 'basic|extended|full' full gives diff
296 296 info details like the diff itself, and number of changed files etc.
297 297 :type details: Optional(str)
298 298
299 299 """
300 300 repo = get_repo_or_error(repoid)
301 301 if not has_superadmin_permission(apiuser):
302 302 _perms = (
303 303 'repository.admin', 'repository.write', 'repository.read',)
304 304 validate_repo_permissions(apiuser, repoid, repo, _perms)
305 305
306 306 changes_details = Optional.extract(details)
307 307 _changes_details_types = ['basic', 'extended', 'full']
308 308 if changes_details not in _changes_details_types:
309 309 raise JSONRPCError(
310 310 'ret_type must be one of %s' % (
311 311 ','.join(_changes_details_types)))
312 312
313 313 pre_load = ['author', 'branch', 'date', 'message', 'parents',
314 314 'status', '_commit', '_file_paths']
315 315
316 316 try:
317 317 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
318 318 except TypeError as e:
319 319 raise JSONRPCError(e.message)
320 320 _cs_json = cs.__json__()
321 321 _cs_json['diff'] = build_commit_data(cs, changes_details)
322 322 if changes_details == 'full':
323 323 _cs_json['refs'] = cs._get_refs()
324 324 return _cs_json
325 325
326 326
327 327 @jsonrpc_method()
328 328 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
329 329 details=Optional('basic')):
330 330 """
331 331 Returns a set of commits limited by the number starting
332 332 from the `start_rev` option.
333 333
334 334 Additional parameters define the amount of details returned by this
335 335 function.
336 336
337 337 This command can only be run using an |authtoken| with admin rights,
338 338 or users with at least read rights to |repos|.
339 339
340 340 :param apiuser: This is filled automatically from the |authtoken|.
341 341 :type apiuser: AuthUser
342 342 :param repoid: The repository name or repository ID.
343 343 :type repoid: str or int
344 344 :param start_rev: The starting revision from where to get changesets.
345 345 :type start_rev: str
346 346 :param limit: Limit the number of commits to this amount
347 347 :type limit: str or int
348 348 :param details: Set the level of detail returned. Valid option are:
349 349 ``basic``, ``extended`` and ``full``.
350 350 :type details: Optional(str)
351 351
352 352 .. note::
353 353
354 354 Setting the parameter `details` to the value ``full`` is extensive
355 355 and returns details like the diff itself, and the number
356 356 of changed files.
357 357
358 358 """
359 359 repo = get_repo_or_error(repoid)
360 360 if not has_superadmin_permission(apiuser):
361 361 _perms = (
362 362 'repository.admin', 'repository.write', 'repository.read',)
363 363 validate_repo_permissions(apiuser, repoid, repo, _perms)
364 364
365 365 changes_details = Optional.extract(details)
366 366 _changes_details_types = ['basic', 'extended', 'full']
367 367 if changes_details not in _changes_details_types:
368 368 raise JSONRPCError(
369 369 'ret_type must be one of %s' % (
370 370 ','.join(_changes_details_types)))
371 371
372 372 limit = int(limit)
373 373 pre_load = ['author', 'branch', 'date', 'message', 'parents',
374 374 'status', '_commit', '_file_paths']
375 375
376 376 vcs_repo = repo.scm_instance()
377 377 # SVN needs a special case to distinguish its index and commit id
378 378 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
379 379 start_rev = vcs_repo.commit_ids[0]
380 380
381 381 try:
382 382 commits = vcs_repo.get_commits(
383 383 start_id=start_rev, pre_load=pre_load)
384 384 except TypeError as e:
385 385 raise JSONRPCError(e.message)
386 386 except Exception:
387 387 log.exception('Fetching of commits failed')
388 388 raise JSONRPCError('Error occurred during commit fetching')
389 389
390 390 ret = []
391 391 for cnt, commit in enumerate(commits):
392 392 if cnt >= limit != -1:
393 393 break
394 394 _cs_json = commit.__json__()
395 395 _cs_json['diff'] = build_commit_data(commit, changes_details)
396 396 if changes_details == 'full':
397 397 _cs_json['refs'] = {
398 398 'branches': [commit.branch],
399 399 'bookmarks': getattr(commit, 'bookmarks', []),
400 400 'tags': commit.tags
401 401 }
402 402 ret.append(_cs_json)
403 403 return ret
404 404
405 405
406 406 @jsonrpc_method()
407 407 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
408 408 ret_type=Optional('all'), details=Optional('basic'),
409 409 max_file_bytes=Optional(None)):
410 410 """
411 411 Returns a list of nodes and children in a flat list for a given
412 412 path at given revision.
413 413
414 414 It's possible to specify ret_type to show only `files` or `dirs`.
415 415
416 416 This command can only be run using an |authtoken| with admin rights,
417 417 or users with at least read rights to |repos|.
418 418
419 419 :param apiuser: This is filled automatically from the |authtoken|.
420 420 :type apiuser: AuthUser
421 421 :param repoid: The repository name or repository ID.
422 422 :type repoid: str or int
423 423 :param revision: The revision for which listing should be done.
424 424 :type revision: str
425 425 :param root_path: The path from which to start displaying.
426 426 :type root_path: str
427 427 :param ret_type: Set the return type. Valid options are
428 428 ``all`` (default), ``files`` and ``dirs``.
429 429 :type ret_type: Optional(str)
430 430 :param details: Returns extended information about nodes, such as
431 431 md5, binary, and or content. The valid options are ``basic`` and
432 432 ``full``.
433 433 :type details: Optional(str)
434 434 :param max_file_bytes: Only return file content under this file size bytes
435 435 :type details: Optional(int)
436 436
437 437 Example output:
438 438
439 439 .. code-block:: bash
440 440
441 441 id : <id_given_in_input>
442 442 result: [
443 443 {
444 444 "name" : "<name>"
445 445 "type" : "<type>",
446 446 "binary": "<true|false>" (only in extended mode)
447 447 "md5" : "<md5 of file content>" (only in extended mode)
448 448 },
449 449 ...
450 450 ]
451 451 error: null
452 452 """
453 453
454 454 repo = get_repo_or_error(repoid)
455 455 if not has_superadmin_permission(apiuser):
456 456 _perms = (
457 457 'repository.admin', 'repository.write', 'repository.read',)
458 458 validate_repo_permissions(apiuser, repoid, repo, _perms)
459 459
460 460 ret_type = Optional.extract(ret_type)
461 461 details = Optional.extract(details)
462 462 _extended_types = ['basic', 'full']
463 463 if details not in _extended_types:
464 464 raise JSONRPCError(
465 465 'ret_type must be one of %s' % (','.join(_extended_types)))
466 466 extended_info = False
467 467 content = False
468 468 if details == 'basic':
469 469 extended_info = True
470 470
471 471 if details == 'full':
472 472 extended_info = content = True
473 473
474 474 _map = {}
475 475 try:
476 476 # check if repo is not empty by any chance, skip quicker if it is.
477 477 _scm = repo.scm_instance()
478 478 if _scm.is_empty():
479 479 return []
480 480
481 481 _d, _f = ScmModel().get_nodes(
482 482 repo, revision, root_path, flat=False,
483 483 extended_info=extended_info, content=content,
484 484 max_file_bytes=max_file_bytes)
485 485 _map = {
486 486 'all': _d + _f,
487 487 'files': _f,
488 488 'dirs': _d,
489 489 }
490 490 return _map[ret_type]
491 491 except KeyError:
492 492 raise JSONRPCError(
493 493 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
494 494 except Exception:
495 495 log.exception("Exception occurred while trying to get repo nodes")
496 496 raise JSONRPCError(
497 497 'failed to get repo: `%s` nodes' % repo.repo_name
498 498 )
499 499
500 500
501 501 @jsonrpc_method()
502 502 def get_repo_refs(request, apiuser, repoid):
503 503 """
504 504 Returns a dictionary of current references. It returns
505 505 bookmarks, branches, closed_branches, and tags for given repository
506 506
507 507 It's possible to specify ret_type to show only `files` or `dirs`.
508 508
509 509 This command can only be run using an |authtoken| with admin rights,
510 510 or users with at least read rights to |repos|.
511 511
512 512 :param apiuser: This is filled automatically from the |authtoken|.
513 513 :type apiuser: AuthUser
514 514 :param repoid: The repository name or repository ID.
515 515 :type repoid: str or int
516 516
517 517 Example output:
518 518
519 519 .. code-block:: bash
520 520
521 521 id : <id_given_in_input>
522 522 "result": {
523 523 "bookmarks": {
524 524 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
525 525 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
526 526 },
527 527 "branches": {
528 528 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
529 529 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
530 530 },
531 531 "branches_closed": {},
532 532 "tags": {
533 533 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
534 534 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
535 535 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
536 536 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
537 537 }
538 538 }
539 539 error: null
540 540 """
541 541
542 542 repo = get_repo_or_error(repoid)
543 543 if not has_superadmin_permission(apiuser):
544 544 _perms = ('repository.admin', 'repository.write', 'repository.read',)
545 545 validate_repo_permissions(apiuser, repoid, repo, _perms)
546 546
547 547 try:
548 548 # check if repo is not empty by any chance, skip quicker if it is.
549 549 vcs_instance = repo.scm_instance()
550 550 refs = vcs_instance.refs()
551 551 return refs
552 552 except Exception:
553 553 log.exception("Exception occurred while trying to get repo refs")
554 554 raise JSONRPCError(
555 555 'failed to get repo: `%s` references' % repo.repo_name
556 556 )
557 557
558 558
559 559 @jsonrpc_method()
560 560 def create_repo(
561 561 request, apiuser, repo_name, repo_type,
562 562 owner=Optional(OAttr('apiuser')),
563 563 description=Optional(''),
564 564 private=Optional(False),
565 565 clone_uri=Optional(None),
566 566 landing_rev=Optional('rev:tip'),
567 567 enable_statistics=Optional(False),
568 568 enable_locking=Optional(False),
569 569 enable_downloads=Optional(False),
570 570 copy_permissions=Optional(False)):
571 571 """
572 572 Creates a repository.
573 573
574 574 * If the repository name contains "/", repository will be created inside
575 575 a repository group or nested repository groups
576 576
577 577 For example "foo/bar/repo1" will create |repo| called "repo1" inside
578 578 group "foo/bar". You have to have permissions to access and write to
579 579 the last repository group ("bar" in this example)
580 580
581 581 This command can only be run using an |authtoken| with at least
582 582 permissions to create repositories, or write permissions to
583 583 parent repository groups.
584 584
585 585 :param apiuser: This is filled automatically from the |authtoken|.
586 586 :type apiuser: AuthUser
587 587 :param repo_name: Set the repository name.
588 588 :type repo_name: str
589 589 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
590 590 :type repo_type: str
591 591 :param owner: user_id or username
592 592 :type owner: Optional(str)
593 593 :param description: Set the repository description.
594 594 :type description: Optional(str)
595 595 :param private: set repository as private
596 596 :type private: bool
597 597 :param clone_uri: set clone_uri
598 598 :type clone_uri: str
599 599 :param landing_rev: <rev_type>:<rev>
600 600 :type landing_rev: str
601 601 :param enable_locking:
602 602 :type enable_locking: bool
603 603 :param enable_downloads:
604 604 :type enable_downloads: bool
605 605 :param enable_statistics:
606 606 :type enable_statistics: bool
607 607 :param copy_permissions: Copy permission from group in which the
608 608 repository is being created.
609 609 :type copy_permissions: bool
610 610
611 611
612 612 Example output:
613 613
614 614 .. code-block:: bash
615 615
616 616 id : <id_given_in_input>
617 617 result: {
618 618 "msg": "Created new repository `<reponame>`",
619 619 "success": true,
620 620 "task": "<celery task id or None if done sync>"
621 621 }
622 622 error: null
623 623
624 624
625 625 Example error output:
626 626
627 627 .. code-block:: bash
628 628
629 629 id : <id_given_in_input>
630 630 result : null
631 631 error : {
632 632 'failed to create repository `<repo_name>`'
633 633 }
634 634
635 635 """
636 636
637 637 owner = validate_set_owner_permissions(apiuser, owner)
638 638
639 639 description = Optional.extract(description)
640 640 copy_permissions = Optional.extract(copy_permissions)
641 641 clone_uri = Optional.extract(clone_uri)
642 642 landing_commit_ref = Optional.extract(landing_rev)
643 643
644 644 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
645 645 if isinstance(private, Optional):
646 646 private = defs.get('repo_private') or Optional.extract(private)
647 647 if isinstance(repo_type, Optional):
648 648 repo_type = defs.get('repo_type')
649 649 if isinstance(enable_statistics, Optional):
650 650 enable_statistics = defs.get('repo_enable_statistics')
651 651 if isinstance(enable_locking, Optional):
652 652 enable_locking = defs.get('repo_enable_locking')
653 653 if isinstance(enable_downloads, Optional):
654 654 enable_downloads = defs.get('repo_enable_downloads')
655 655
656 656 schema = repo_schema.RepoSchema().bind(
657 657 repo_type_options=rhodecode.BACKENDS.keys(),
658 repo_type=repo_type,
658 659 # user caller
659 660 user=apiuser)
660 661
661 662 try:
662 663 schema_data = schema.deserialize(dict(
663 664 repo_name=repo_name,
664 665 repo_type=repo_type,
665 666 repo_owner=owner.username,
666 667 repo_description=description,
667 668 repo_landing_commit_ref=landing_commit_ref,
668 669 repo_clone_uri=clone_uri,
669 670 repo_private=private,
670 671 repo_copy_permissions=copy_permissions,
671 672 repo_enable_statistics=enable_statistics,
672 673 repo_enable_downloads=enable_downloads,
673 674 repo_enable_locking=enable_locking))
674 675 except validation_schema.Invalid as err:
675 676 raise JSONRPCValidationError(colander_exc=err)
676 677
677 678 try:
678 679 data = {
679 680 'owner': owner,
680 681 'repo_name': schema_data['repo_group']['repo_name_without_group'],
681 682 'repo_name_full': schema_data['repo_name'],
682 683 'repo_group': schema_data['repo_group']['repo_group_id'],
683 684 'repo_type': schema_data['repo_type'],
684 685 'repo_description': schema_data['repo_description'],
685 686 'repo_private': schema_data['repo_private'],
686 687 'clone_uri': schema_data['repo_clone_uri'],
687 688 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
688 689 'enable_statistics': schema_data['repo_enable_statistics'],
689 690 'enable_locking': schema_data['repo_enable_locking'],
690 691 'enable_downloads': schema_data['repo_enable_downloads'],
691 692 'repo_copy_permissions': schema_data['repo_copy_permissions'],
692 693 }
693 694
694 695 task = RepoModel().create(form_data=data, cur_user=owner)
695 696 task_id = get_task_id(task)
696 697 # no commit, it's done in RepoModel, or async via celery
697 698 return {
698 699 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
699 700 'success': True, # cannot return the repo data here since fork
700 701 # can be done async
701 702 'task': task_id
702 703 }
703 704 except Exception:
704 705 log.exception(
705 706 u"Exception while trying to create the repository %s",
706 707 schema_data['repo_name'])
707 708 raise JSONRPCError(
708 709 'failed to create repository `%s`' % (schema_data['repo_name'],))
709 710
710 711
711 712 @jsonrpc_method()
712 713 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
713 714 description=Optional('')):
714 715 """
715 716 Adds an extra field to a repository.
716 717
717 718 This command can only be run using an |authtoken| with at least
718 719 write permissions to the |repo|.
719 720
720 721 :param apiuser: This is filled automatically from the |authtoken|.
721 722 :type apiuser: AuthUser
722 723 :param repoid: Set the repository name or repository id.
723 724 :type repoid: str or int
724 725 :param key: Create a unique field key for this repository.
725 726 :type key: str
726 727 :param label:
727 728 :type label: Optional(str)
728 729 :param description:
729 730 :type description: Optional(str)
730 731 """
731 732 repo = get_repo_or_error(repoid)
732 733 if not has_superadmin_permission(apiuser):
733 734 _perms = ('repository.admin',)
734 735 validate_repo_permissions(apiuser, repoid, repo, _perms)
735 736
736 737 label = Optional.extract(label) or key
737 738 description = Optional.extract(description)
738 739
739 740 field = RepositoryField.get_by_key_name(key, repo)
740 741 if field:
741 742 raise JSONRPCError('Field with key '
742 743 '`%s` exists for repo `%s`' % (key, repoid))
743 744
744 745 try:
745 746 RepoModel().add_repo_field(repo, key, field_label=label,
746 747 field_desc=description)
747 748 Session().commit()
748 749 return {
749 750 'msg': "Added new repository field `%s`" % (key,),
750 751 'success': True,
751 752 }
752 753 except Exception:
753 754 log.exception("Exception occurred while trying to add field to repo")
754 755 raise JSONRPCError(
755 756 'failed to create new field for repository `%s`' % (repoid,))
756 757
757 758
758 759 @jsonrpc_method()
759 760 def remove_field_from_repo(request, apiuser, repoid, key):
760 761 """
761 762 Removes an extra field from a repository.
762 763
763 764 This command can only be run using an |authtoken| with at least
764 765 write permissions to the |repo|.
765 766
766 767 :param apiuser: This is filled automatically from the |authtoken|.
767 768 :type apiuser: AuthUser
768 769 :param repoid: Set the repository name or repository ID.
769 770 :type repoid: str or int
770 771 :param key: Set the unique field key for this repository.
771 772 :type key: str
772 773 """
773 774
774 775 repo = get_repo_or_error(repoid)
775 776 if not has_superadmin_permission(apiuser):
776 777 _perms = ('repository.admin',)
777 778 validate_repo_permissions(apiuser, repoid, repo, _perms)
778 779
779 780 field = RepositoryField.get_by_key_name(key, repo)
780 781 if not field:
781 782 raise JSONRPCError('Field with key `%s` does not '
782 783 'exists for repo `%s`' % (key, repoid))
783 784
784 785 try:
785 786 RepoModel().delete_repo_field(repo, field_key=key)
786 787 Session().commit()
787 788 return {
788 789 'msg': "Deleted repository field `%s`" % (key,),
789 790 'success': True,
790 791 }
791 792 except Exception:
792 793 log.exception(
793 794 "Exception occurred while trying to delete field from repo")
794 795 raise JSONRPCError(
795 796 'failed to delete field for repository `%s`' % (repoid,))
796 797
797 798
798 799 @jsonrpc_method()
799 800 def update_repo(
800 801 request, apiuser, repoid, repo_name=Optional(None),
801 802 owner=Optional(OAttr('apiuser')), description=Optional(''),
802 803 private=Optional(False), clone_uri=Optional(None),
803 804 landing_rev=Optional('rev:tip'), fork_of=Optional(None),
804 805 enable_statistics=Optional(False),
805 806 enable_locking=Optional(False),
806 807 enable_downloads=Optional(False), fields=Optional('')):
807 808 """
808 809 Updates a repository with the given information.
809 810
810 811 This command can only be run using an |authtoken| with at least
811 812 admin permissions to the |repo|.
812 813
813 814 * If the repository name contains "/", repository will be updated
814 815 accordingly with a repository group or nested repository groups
815 816
816 817 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
817 818 called "repo-test" and place it inside group "foo/bar".
818 819 You have to have permissions to access and write to the last repository
819 820 group ("bar" in this example)
820 821
821 822 :param apiuser: This is filled automatically from the |authtoken|.
822 823 :type apiuser: AuthUser
823 824 :param repoid: repository name or repository ID.
824 825 :type repoid: str or int
825 826 :param repo_name: Update the |repo| name, including the
826 827 repository group it's in.
827 828 :type repo_name: str
828 829 :param owner: Set the |repo| owner.
829 830 :type owner: str
830 831 :param fork_of: Set the |repo| as fork of another |repo|.
831 832 :type fork_of: str
832 833 :param description: Update the |repo| description.
833 834 :type description: str
834 835 :param private: Set the |repo| as private. (True | False)
835 836 :type private: bool
836 837 :param clone_uri: Update the |repo| clone URI.
837 838 :type clone_uri: str
838 839 :param landing_rev: Set the |repo| landing revision. Default is ``rev:tip``.
839 840 :type landing_rev: str
840 841 :param enable_statistics: Enable statistics on the |repo|, (True | False).
841 842 :type enable_statistics: bool
842 843 :param enable_locking: Enable |repo| locking.
843 844 :type enable_locking: bool
844 845 :param enable_downloads: Enable downloads from the |repo|, (True | False).
845 846 :type enable_downloads: bool
846 847 :param fields: Add extra fields to the |repo|. Use the following
847 848 example format: ``field_key=field_val,field_key2=fieldval2``.
848 849 Escape ', ' with \,
849 850 :type fields: str
850 851 """
851 852
852 853 repo = get_repo_or_error(repoid)
853 854
854 855 include_secrets = False
855 856 if not has_superadmin_permission(apiuser):
856 857 validate_repo_permissions(apiuser, repoid, repo, ('repository.admin',))
857 858 else:
858 859 include_secrets = True
859 860
860 861 updates = dict(
861 862 repo_name=repo_name
862 863 if not isinstance(repo_name, Optional) else repo.repo_name,
863 864
864 865 fork_id=fork_of
865 866 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
866 867
867 868 user=owner
868 869 if not isinstance(owner, Optional) else repo.user.username,
869 870
870 871 repo_description=description
871 872 if not isinstance(description, Optional) else repo.description,
872 873
873 874 repo_private=private
874 875 if not isinstance(private, Optional) else repo.private,
875 876
876 877 clone_uri=clone_uri
877 878 if not isinstance(clone_uri, Optional) else repo.clone_uri,
878 879
879 880 repo_landing_rev=landing_rev
880 881 if not isinstance(landing_rev, Optional) else repo._landing_revision,
881 882
882 883 repo_enable_statistics=enable_statistics
883 884 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
884 885
885 886 repo_enable_locking=enable_locking
886 887 if not isinstance(enable_locking, Optional) else repo.enable_locking,
887 888
888 889 repo_enable_downloads=enable_downloads
889 890 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
890 891
891 892 ref_choices, _labels = ScmModel().get_repo_landing_revs(
892 893 request.translate, repo=repo)
893 894
894 895 old_values = repo.get_api_data()
896 repo_type = repo.repo_type
895 897 schema = repo_schema.RepoSchema().bind(
896 898 repo_type_options=rhodecode.BACKENDS.keys(),
897 899 repo_ref_options=ref_choices,
900 repo_type=repo_type,
898 901 # user caller
899 902 user=apiuser,
900 903 old_values=old_values)
901 904 try:
902 905 schema_data = schema.deserialize(dict(
903 906 # we save old value, users cannot change type
904 repo_type=repo.repo_type,
907 repo_type=repo_type,
905 908
906 909 repo_name=updates['repo_name'],
907 910 repo_owner=updates['user'],
908 911 repo_description=updates['repo_description'],
909 912 repo_clone_uri=updates['clone_uri'],
910 913 repo_fork_of=updates['fork_id'],
911 914 repo_private=updates['repo_private'],
912 915 repo_landing_commit_ref=updates['repo_landing_rev'],
913 916 repo_enable_statistics=updates['repo_enable_statistics'],
914 917 repo_enable_downloads=updates['repo_enable_downloads'],
915 918 repo_enable_locking=updates['repo_enable_locking']))
916 919 except validation_schema.Invalid as err:
917 920 raise JSONRPCValidationError(colander_exc=err)
918 921
919 922 # save validated data back into the updates dict
920 923 validated_updates = dict(
921 924 repo_name=schema_data['repo_group']['repo_name_without_group'],
922 925 repo_group=schema_data['repo_group']['repo_group_id'],
923 926
924 927 user=schema_data['repo_owner'],
925 928 repo_description=schema_data['repo_description'],
926 929 repo_private=schema_data['repo_private'],
927 930 clone_uri=schema_data['repo_clone_uri'],
928 931 repo_landing_rev=schema_data['repo_landing_commit_ref'],
929 932 repo_enable_statistics=schema_data['repo_enable_statistics'],
930 933 repo_enable_locking=schema_data['repo_enable_locking'],
931 934 repo_enable_downloads=schema_data['repo_enable_downloads'],
932 935 )
933 936
934 937 if schema_data['repo_fork_of']:
935 938 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
936 939 validated_updates['fork_id'] = fork_repo.repo_id
937 940
938 941 # extra fields
939 942 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
940 943 if fields:
941 944 validated_updates.update(fields)
942 945
943 946 try:
944 947 RepoModel().update(repo, **validated_updates)
945 948 audit_logger.store_api(
946 949 'repo.edit', action_data={'old_data': old_values},
947 950 user=apiuser, repo=repo)
948 951 Session().commit()
949 952 return {
950 953 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
951 954 'repository': repo.get_api_data(include_secrets=include_secrets)
952 955 }
953 956 except Exception:
954 957 log.exception(
955 958 u"Exception while trying to update the repository %s",
956 959 repoid)
957 960 raise JSONRPCError('failed to update repo `%s`' % repoid)
958 961
959 962
960 963 @jsonrpc_method()
961 964 def fork_repo(request, apiuser, repoid, fork_name,
962 965 owner=Optional(OAttr('apiuser')),
963 966 description=Optional(''),
964 967 private=Optional(False),
965 968 clone_uri=Optional(None),
966 969 landing_rev=Optional('rev:tip'),
967 970 copy_permissions=Optional(False)):
968 971 """
969 972 Creates a fork of the specified |repo|.
970 973
971 974 * If the fork_name contains "/", fork will be created inside
972 975 a repository group or nested repository groups
973 976
974 977 For example "foo/bar/fork-repo" will create fork called "fork-repo"
975 978 inside group "foo/bar". You have to have permissions to access and
976 979 write to the last repository group ("bar" in this example)
977 980
978 981 This command can only be run using an |authtoken| with minimum
979 982 read permissions of the forked repo, create fork permissions for an user.
980 983
981 984 :param apiuser: This is filled automatically from the |authtoken|.
982 985 :type apiuser: AuthUser
983 986 :param repoid: Set repository name or repository ID.
984 987 :type repoid: str or int
985 988 :param fork_name: Set the fork name, including it's repository group membership.
986 989 :type fork_name: str
987 990 :param owner: Set the fork owner.
988 991 :type owner: str
989 992 :param description: Set the fork description.
990 993 :type description: str
991 994 :param copy_permissions: Copy permissions from parent |repo|. The
992 995 default is False.
993 996 :type copy_permissions: bool
994 997 :param private: Make the fork private. The default is False.
995 998 :type private: bool
996 999 :param landing_rev: Set the landing revision. The default is tip.
997 1000
998 1001 Example output:
999 1002
1000 1003 .. code-block:: bash
1001 1004
1002 1005 id : <id_for_response>
1003 1006 api_key : "<api_key>"
1004 1007 args: {
1005 1008 "repoid" : "<reponame or repo_id>",
1006 1009 "fork_name": "<forkname>",
1007 1010 "owner": "<username or user_id = Optional(=apiuser)>",
1008 1011 "description": "<description>",
1009 1012 "copy_permissions": "<bool>",
1010 1013 "private": "<bool>",
1011 1014 "landing_rev": "<landing_rev>"
1012 1015 }
1013 1016
1014 1017 Example error output:
1015 1018
1016 1019 .. code-block:: bash
1017 1020
1018 1021 id : <id_given_in_input>
1019 1022 result: {
1020 1023 "msg": "Created fork of `<reponame>` as `<forkname>`",
1021 1024 "success": true,
1022 1025 "task": "<celery task id or None if done sync>"
1023 1026 }
1024 1027 error: null
1025 1028
1026 1029 """
1027 1030
1028 1031 repo = get_repo_or_error(repoid)
1029 1032 repo_name = repo.repo_name
1030 1033
1031 1034 if not has_superadmin_permission(apiuser):
1032 1035 # check if we have at least read permission for
1033 1036 # this repo that we fork !
1034 1037 _perms = (
1035 1038 'repository.admin', 'repository.write', 'repository.read')
1036 1039 validate_repo_permissions(apiuser, repoid, repo, _perms)
1037 1040
1038 1041 # check if the regular user has at least fork permissions as well
1039 1042 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1040 1043 raise JSONRPCForbidden()
1041 1044
1042 1045 # check if user can set owner parameter
1043 1046 owner = validate_set_owner_permissions(apiuser, owner)
1044 1047
1045 1048 description = Optional.extract(description)
1046 1049 copy_permissions = Optional.extract(copy_permissions)
1047 1050 clone_uri = Optional.extract(clone_uri)
1048 1051 landing_commit_ref = Optional.extract(landing_rev)
1049 1052 private = Optional.extract(private)
1050 1053
1051 1054 schema = repo_schema.RepoSchema().bind(
1052 1055 repo_type_options=rhodecode.BACKENDS.keys(),
1056 repo_type=repo.repo_type,
1053 1057 # user caller
1054 1058 user=apiuser)
1055 1059
1056 1060 try:
1057 1061 schema_data = schema.deserialize(dict(
1058 1062 repo_name=fork_name,
1059 1063 repo_type=repo.repo_type,
1060 1064 repo_owner=owner.username,
1061 1065 repo_description=description,
1062 1066 repo_landing_commit_ref=landing_commit_ref,
1063 1067 repo_clone_uri=clone_uri,
1064 1068 repo_private=private,
1065 1069 repo_copy_permissions=copy_permissions))
1066 1070 except validation_schema.Invalid as err:
1067 1071 raise JSONRPCValidationError(colander_exc=err)
1068 1072
1069 1073 try:
1070 1074 data = {
1071 1075 'fork_parent_id': repo.repo_id,
1072 1076
1073 1077 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1074 1078 'repo_name_full': schema_data['repo_name'],
1075 1079 'repo_group': schema_data['repo_group']['repo_group_id'],
1076 1080 'repo_type': schema_data['repo_type'],
1077 1081 'description': schema_data['repo_description'],
1078 1082 'private': schema_data['repo_private'],
1079 1083 'copy_permissions': schema_data['repo_copy_permissions'],
1080 1084 'landing_rev': schema_data['repo_landing_commit_ref'],
1081 1085 }
1082 1086
1083 1087 task = RepoModel().create_fork(data, cur_user=owner)
1084 1088 # no commit, it's done in RepoModel, or async via celery
1085 1089 task_id = get_task_id(task)
1086 1090
1087 1091 return {
1088 1092 'msg': 'Created fork of `%s` as `%s`' % (
1089 1093 repo.repo_name, schema_data['repo_name']),
1090 1094 'success': True, # cannot return the repo data here since fork
1091 1095 # can be done async
1092 1096 'task': task_id
1093 1097 }
1094 1098 except Exception:
1095 1099 log.exception(
1096 1100 u"Exception while trying to create fork %s",
1097 1101 schema_data['repo_name'])
1098 1102 raise JSONRPCError(
1099 1103 'failed to fork repository `%s` as `%s`' % (
1100 1104 repo_name, schema_data['repo_name']))
1101 1105
1102 1106
1103 1107 @jsonrpc_method()
1104 1108 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1105 1109 """
1106 1110 Deletes a repository.
1107 1111
1108 1112 * When the `forks` parameter is set it's possible to detach or delete
1109 1113 forks of deleted repository.
1110 1114
1111 1115 This command can only be run using an |authtoken| with admin
1112 1116 permissions on the |repo|.
1113 1117
1114 1118 :param apiuser: This is filled automatically from the |authtoken|.
1115 1119 :type apiuser: AuthUser
1116 1120 :param repoid: Set the repository name or repository ID.
1117 1121 :type repoid: str or int
1118 1122 :param forks: Set to `detach` or `delete` forks from the |repo|.
1119 1123 :type forks: Optional(str)
1120 1124
1121 1125 Example error output:
1122 1126
1123 1127 .. code-block:: bash
1124 1128
1125 1129 id : <id_given_in_input>
1126 1130 result: {
1127 1131 "msg": "Deleted repository `<reponame>`",
1128 1132 "success": true
1129 1133 }
1130 1134 error: null
1131 1135 """
1132 1136
1133 1137 repo = get_repo_or_error(repoid)
1134 1138 repo_name = repo.repo_name
1135 1139 if not has_superadmin_permission(apiuser):
1136 1140 _perms = ('repository.admin',)
1137 1141 validate_repo_permissions(apiuser, repoid, repo, _perms)
1138 1142
1139 1143 try:
1140 1144 handle_forks = Optional.extract(forks)
1141 1145 _forks_msg = ''
1142 1146 _forks = [f for f in repo.forks]
1143 1147 if handle_forks == 'detach':
1144 1148 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1145 1149 elif handle_forks == 'delete':
1146 1150 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1147 1151 elif _forks:
1148 1152 raise JSONRPCError(
1149 1153 'Cannot delete `%s` it still contains attached forks' %
1150 1154 (repo.repo_name,)
1151 1155 )
1152 1156 old_data = repo.get_api_data()
1153 1157 RepoModel().delete(repo, forks=forks)
1154 1158
1155 1159 repo = audit_logger.RepoWrap(repo_id=None,
1156 1160 repo_name=repo.repo_name)
1157 1161
1158 1162 audit_logger.store_api(
1159 1163 'repo.delete', action_data={'old_data': old_data},
1160 1164 user=apiuser, repo=repo)
1161 1165
1162 1166 ScmModel().mark_for_invalidation(repo_name, delete=True)
1163 1167 Session().commit()
1164 1168 return {
1165 1169 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
1166 1170 'success': True
1167 1171 }
1168 1172 except Exception:
1169 1173 log.exception("Exception occurred while trying to delete repo")
1170 1174 raise JSONRPCError(
1171 1175 'failed to delete repository `%s`' % (repo_name,)
1172 1176 )
1173 1177
1174 1178
1175 1179 #TODO: marcink, change name ?
1176 1180 @jsonrpc_method()
1177 1181 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1178 1182 """
1179 1183 Invalidates the cache for the specified repository.
1180 1184
1181 1185 This command can only be run using an |authtoken| with admin rights to
1182 1186 the specified repository.
1183 1187
1184 1188 This command takes the following options:
1185 1189
1186 1190 :param apiuser: This is filled automatically from |authtoken|.
1187 1191 :type apiuser: AuthUser
1188 1192 :param repoid: Sets the repository name or repository ID.
1189 1193 :type repoid: str or int
1190 1194 :param delete_keys: This deletes the invalidated keys instead of
1191 1195 just flagging them.
1192 1196 :type delete_keys: Optional(``True`` | ``False``)
1193 1197
1194 1198 Example output:
1195 1199
1196 1200 .. code-block:: bash
1197 1201
1198 1202 id : <id_given_in_input>
1199 1203 result : {
1200 1204 'msg': Cache for repository `<repository name>` was invalidated,
1201 1205 'repository': <repository name>
1202 1206 }
1203 1207 error : null
1204 1208
1205 1209 Example error output:
1206 1210
1207 1211 .. code-block:: bash
1208 1212
1209 1213 id : <id_given_in_input>
1210 1214 result : null
1211 1215 error : {
1212 1216 'Error occurred during cache invalidation action'
1213 1217 }
1214 1218
1215 1219 """
1216 1220
1217 1221 repo = get_repo_or_error(repoid)
1218 1222 if not has_superadmin_permission(apiuser):
1219 1223 _perms = ('repository.admin', 'repository.write',)
1220 1224 validate_repo_permissions(apiuser, repoid, repo, _perms)
1221 1225
1222 1226 delete = Optional.extract(delete_keys)
1223 1227 try:
1224 1228 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1225 1229 return {
1226 1230 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1227 1231 'repository': repo.repo_name
1228 1232 }
1229 1233 except Exception:
1230 1234 log.exception(
1231 1235 "Exception occurred while trying to invalidate repo cache")
1232 1236 raise JSONRPCError(
1233 1237 'Error occurred during cache invalidation action'
1234 1238 )
1235 1239
1236 1240
1237 1241 #TODO: marcink, change name ?
1238 1242 @jsonrpc_method()
1239 1243 def lock(request, apiuser, repoid, locked=Optional(None),
1240 1244 userid=Optional(OAttr('apiuser'))):
1241 1245 """
1242 1246 Sets the lock state of the specified |repo| by the given user.
1243 1247 From more information, see :ref:`repo-locking`.
1244 1248
1245 1249 * If the ``userid`` option is not set, the repository is locked to the
1246 1250 user who called the method.
1247 1251 * If the ``locked`` parameter is not set, the current lock state of the
1248 1252 repository is displayed.
1249 1253
1250 1254 This command can only be run using an |authtoken| with admin rights to
1251 1255 the specified repository.
1252 1256
1253 1257 This command takes the following options:
1254 1258
1255 1259 :param apiuser: This is filled automatically from the |authtoken|.
1256 1260 :type apiuser: AuthUser
1257 1261 :param repoid: Sets the repository name or repository ID.
1258 1262 :type repoid: str or int
1259 1263 :param locked: Sets the lock state.
1260 1264 :type locked: Optional(``True`` | ``False``)
1261 1265 :param userid: Set the repository lock to this user.
1262 1266 :type userid: Optional(str or int)
1263 1267
1264 1268 Example error output:
1265 1269
1266 1270 .. code-block:: bash
1267 1271
1268 1272 id : <id_given_in_input>
1269 1273 result : {
1270 1274 'repo': '<reponame>',
1271 1275 'locked': <bool: lock state>,
1272 1276 'locked_since': <int: lock timestamp>,
1273 1277 'locked_by': <username of person who made the lock>,
1274 1278 'lock_reason': <str: reason for locking>,
1275 1279 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1276 1280 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1277 1281 or
1278 1282 'msg': 'Repo `<repository name>` not locked.'
1279 1283 or
1280 1284 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1281 1285 }
1282 1286 error : null
1283 1287
1284 1288 Example error output:
1285 1289
1286 1290 .. code-block:: bash
1287 1291
1288 1292 id : <id_given_in_input>
1289 1293 result : null
1290 1294 error : {
1291 1295 'Error occurred locking repository `<reponame>`'
1292 1296 }
1293 1297 """
1294 1298
1295 1299 repo = get_repo_or_error(repoid)
1296 1300 if not has_superadmin_permission(apiuser):
1297 1301 # check if we have at least write permission for this repo !
1298 1302 _perms = ('repository.admin', 'repository.write',)
1299 1303 validate_repo_permissions(apiuser, repoid, repo, _perms)
1300 1304
1301 1305 # make sure normal user does not pass someone else userid,
1302 1306 # he is not allowed to do that
1303 1307 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1304 1308 raise JSONRPCError('userid is not the same as your user')
1305 1309
1306 1310 if isinstance(userid, Optional):
1307 1311 userid = apiuser.user_id
1308 1312
1309 1313 user = get_user_or_error(userid)
1310 1314
1311 1315 if isinstance(locked, Optional):
1312 1316 lockobj = repo.locked
1313 1317
1314 1318 if lockobj[0] is None:
1315 1319 _d = {
1316 1320 'repo': repo.repo_name,
1317 1321 'locked': False,
1318 1322 'locked_since': None,
1319 1323 'locked_by': None,
1320 1324 'lock_reason': None,
1321 1325 'lock_state_changed': False,
1322 1326 'msg': 'Repo `%s` not locked.' % repo.repo_name
1323 1327 }
1324 1328 return _d
1325 1329 else:
1326 1330 _user_id, _time, _reason = lockobj
1327 1331 lock_user = get_user_or_error(userid)
1328 1332 _d = {
1329 1333 'repo': repo.repo_name,
1330 1334 'locked': True,
1331 1335 'locked_since': _time,
1332 1336 'locked_by': lock_user.username,
1333 1337 'lock_reason': _reason,
1334 1338 'lock_state_changed': False,
1335 1339 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1336 1340 % (repo.repo_name, lock_user.username,
1337 1341 json.dumps(time_to_datetime(_time))))
1338 1342 }
1339 1343 return _d
1340 1344
1341 1345 # force locked state through a flag
1342 1346 else:
1343 1347 locked = str2bool(locked)
1344 1348 lock_reason = Repository.LOCK_API
1345 1349 try:
1346 1350 if locked:
1347 1351 lock_time = time.time()
1348 1352 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1349 1353 else:
1350 1354 lock_time = None
1351 1355 Repository.unlock(repo)
1352 1356 _d = {
1353 1357 'repo': repo.repo_name,
1354 1358 'locked': locked,
1355 1359 'locked_since': lock_time,
1356 1360 'locked_by': user.username,
1357 1361 'lock_reason': lock_reason,
1358 1362 'lock_state_changed': True,
1359 1363 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1360 1364 % (user.username, repo.repo_name, locked))
1361 1365 }
1362 1366 return _d
1363 1367 except Exception:
1364 1368 log.exception(
1365 1369 "Exception occurred while trying to lock repository")
1366 1370 raise JSONRPCError(
1367 1371 'Error occurred locking repository `%s`' % repo.repo_name
1368 1372 )
1369 1373
1370 1374
1371 1375 @jsonrpc_method()
1372 1376 def comment_commit(
1373 1377 request, apiuser, repoid, commit_id, message, status=Optional(None),
1374 1378 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1375 1379 resolves_comment_id=Optional(None),
1376 1380 userid=Optional(OAttr('apiuser'))):
1377 1381 """
1378 1382 Set a commit comment, and optionally change the status of the commit.
1379 1383
1380 1384 :param apiuser: This is filled automatically from the |authtoken|.
1381 1385 :type apiuser: AuthUser
1382 1386 :param repoid: Set the repository name or repository ID.
1383 1387 :type repoid: str or int
1384 1388 :param commit_id: Specify the commit_id for which to set a comment.
1385 1389 :type commit_id: str
1386 1390 :param message: The comment text.
1387 1391 :type message: str
1388 1392 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1389 1393 'approved', 'rejected', 'under_review'
1390 1394 :type status: str
1391 1395 :param comment_type: Comment type, one of: 'note', 'todo'
1392 1396 :type comment_type: Optional(str), default: 'note'
1393 1397 :param userid: Set the user name of the comment creator.
1394 1398 :type userid: Optional(str or int)
1395 1399
1396 1400 Example error output:
1397 1401
1398 1402 .. code-block:: bash
1399 1403
1400 1404 {
1401 1405 "id" : <id_given_in_input>,
1402 1406 "result" : {
1403 1407 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1404 1408 "status_change": null or <status>,
1405 1409 "success": true
1406 1410 },
1407 1411 "error" : null
1408 1412 }
1409 1413
1410 1414 """
1411 1415 repo = get_repo_or_error(repoid)
1412 1416 if not has_superadmin_permission(apiuser):
1413 1417 _perms = ('repository.read', 'repository.write', 'repository.admin')
1414 1418 validate_repo_permissions(apiuser, repoid, repo, _perms)
1415 1419
1416 1420 try:
1417 1421 commit_id = repo.scm_instance().get_commit(commit_id=commit_id).raw_id
1418 1422 except Exception as e:
1419 1423 log.exception('Failed to fetch commit')
1420 1424 raise JSONRPCError(e.message)
1421 1425
1422 1426 if isinstance(userid, Optional):
1423 1427 userid = apiuser.user_id
1424 1428
1425 1429 user = get_user_or_error(userid)
1426 1430 status = Optional.extract(status)
1427 1431 comment_type = Optional.extract(comment_type)
1428 1432 resolves_comment_id = Optional.extract(resolves_comment_id)
1429 1433
1430 1434 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1431 1435 if status and status not in allowed_statuses:
1432 1436 raise JSONRPCError('Bad status, must be on '
1433 1437 'of %s got %s' % (allowed_statuses, status,))
1434 1438
1435 1439 if resolves_comment_id:
1436 1440 comment = ChangesetComment.get(resolves_comment_id)
1437 1441 if not comment:
1438 1442 raise JSONRPCError(
1439 1443 'Invalid resolves_comment_id `%s` for this commit.'
1440 1444 % resolves_comment_id)
1441 1445 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1442 1446 raise JSONRPCError(
1443 1447 'Comment `%s` is wrong type for setting status to resolved.'
1444 1448 % resolves_comment_id)
1445 1449
1446 1450 try:
1447 1451 rc_config = SettingsModel().get_all_settings()
1448 1452 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1449 1453 status_change_label = ChangesetStatus.get_status_lbl(status)
1450 1454 comment = CommentsModel().create(
1451 1455 message, repo, user, commit_id=commit_id,
1452 1456 status_change=status_change_label,
1453 1457 status_change_type=status,
1454 1458 renderer=renderer,
1455 1459 comment_type=comment_type,
1456 1460 resolves_comment_id=resolves_comment_id
1457 1461 )
1458 1462 if status:
1459 1463 # also do a status change
1460 1464 try:
1461 1465 ChangesetStatusModel().set_status(
1462 1466 repo, status, user, comment, revision=commit_id,
1463 1467 dont_allow_on_closed_pull_request=True
1464 1468 )
1465 1469 except StatusChangeOnClosedPullRequestError:
1466 1470 log.exception(
1467 1471 "Exception occurred while trying to change repo commit status")
1468 1472 msg = ('Changing status on a changeset associated with '
1469 1473 'a closed pull request is not allowed')
1470 1474 raise JSONRPCError(msg)
1471 1475
1472 1476 Session().commit()
1473 1477 return {
1474 1478 'msg': (
1475 1479 'Commented on commit `%s` for repository `%s`' % (
1476 1480 comment.revision, repo.repo_name)),
1477 1481 'status_change': status,
1478 1482 'success': True,
1479 1483 }
1480 1484 except JSONRPCError:
1481 1485 # catch any inside errors, and re-raise them to prevent from
1482 1486 # below global catch to silence them
1483 1487 raise
1484 1488 except Exception:
1485 1489 log.exception("Exception occurred while trying to comment on commit")
1486 1490 raise JSONRPCError(
1487 1491 'failed to set comment on repository `%s`' % (repo.repo_name,)
1488 1492 )
1489 1493
1490 1494
1491 1495 @jsonrpc_method()
1492 1496 def grant_user_permission(request, apiuser, repoid, userid, perm):
1493 1497 """
1494 1498 Grant permissions for the specified user on the given repository,
1495 1499 or update existing permissions if found.
1496 1500
1497 1501 This command can only be run using an |authtoken| with admin
1498 1502 permissions on the |repo|.
1499 1503
1500 1504 :param apiuser: This is filled automatically from the |authtoken|.
1501 1505 :type apiuser: AuthUser
1502 1506 :param repoid: Set the repository name or repository ID.
1503 1507 :type repoid: str or int
1504 1508 :param userid: Set the user name.
1505 1509 :type userid: str
1506 1510 :param perm: Set the user permissions, using the following format
1507 1511 ``(repository.(none|read|write|admin))``
1508 1512 :type perm: str
1509 1513
1510 1514 Example output:
1511 1515
1512 1516 .. code-block:: bash
1513 1517
1514 1518 id : <id_given_in_input>
1515 1519 result: {
1516 1520 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1517 1521 "success": true
1518 1522 }
1519 1523 error: null
1520 1524 """
1521 1525
1522 1526 repo = get_repo_or_error(repoid)
1523 1527 user = get_user_or_error(userid)
1524 1528 perm = get_perm_or_error(perm)
1525 1529 if not has_superadmin_permission(apiuser):
1526 1530 _perms = ('repository.admin',)
1527 1531 validate_repo_permissions(apiuser, repoid, repo, _perms)
1528 1532
1529 1533 try:
1530 1534
1531 1535 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
1532 1536
1533 1537 Session().commit()
1534 1538 return {
1535 1539 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1536 1540 perm.permission_name, user.username, repo.repo_name
1537 1541 ),
1538 1542 'success': True
1539 1543 }
1540 1544 except Exception:
1541 1545 log.exception(
1542 1546 "Exception occurred while trying edit permissions for repo")
1543 1547 raise JSONRPCError(
1544 1548 'failed to edit permission for user: `%s` in repo: `%s`' % (
1545 1549 userid, repoid
1546 1550 )
1547 1551 )
1548 1552
1549 1553
1550 1554 @jsonrpc_method()
1551 1555 def revoke_user_permission(request, apiuser, repoid, userid):
1552 1556 """
1553 1557 Revoke permission for a user on the specified repository.
1554 1558
1555 1559 This command can only be run using an |authtoken| with admin
1556 1560 permissions on the |repo|.
1557 1561
1558 1562 :param apiuser: This is filled automatically from the |authtoken|.
1559 1563 :type apiuser: AuthUser
1560 1564 :param repoid: Set the repository name or repository ID.
1561 1565 :type repoid: str or int
1562 1566 :param userid: Set the user name of revoked user.
1563 1567 :type userid: str or int
1564 1568
1565 1569 Example error output:
1566 1570
1567 1571 .. code-block:: bash
1568 1572
1569 1573 id : <id_given_in_input>
1570 1574 result: {
1571 1575 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1572 1576 "success": true
1573 1577 }
1574 1578 error: null
1575 1579 """
1576 1580
1577 1581 repo = get_repo_or_error(repoid)
1578 1582 user = get_user_or_error(userid)
1579 1583 if not has_superadmin_permission(apiuser):
1580 1584 _perms = ('repository.admin',)
1581 1585 validate_repo_permissions(apiuser, repoid, repo, _perms)
1582 1586
1583 1587 try:
1584 1588 RepoModel().revoke_user_permission(repo=repo, user=user)
1585 1589 Session().commit()
1586 1590 return {
1587 1591 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1588 1592 user.username, repo.repo_name
1589 1593 ),
1590 1594 'success': True
1591 1595 }
1592 1596 except Exception:
1593 1597 log.exception(
1594 1598 "Exception occurred while trying revoke permissions to repo")
1595 1599 raise JSONRPCError(
1596 1600 'failed to edit permission for user: `%s` in repo: `%s`' % (
1597 1601 userid, repoid
1598 1602 )
1599 1603 )
1600 1604
1601 1605
1602 1606 @jsonrpc_method()
1603 1607 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1604 1608 """
1605 1609 Grant permission for a user group on the specified repository,
1606 1610 or update existing permissions.
1607 1611
1608 1612 This command can only be run using an |authtoken| with admin
1609 1613 permissions on the |repo|.
1610 1614
1611 1615 :param apiuser: This is filled automatically from the |authtoken|.
1612 1616 :type apiuser: AuthUser
1613 1617 :param repoid: Set the repository name or repository ID.
1614 1618 :type repoid: str or int
1615 1619 :param usergroupid: Specify the ID of the user group.
1616 1620 :type usergroupid: str or int
1617 1621 :param perm: Set the user group permissions using the following
1618 1622 format: (repository.(none|read|write|admin))
1619 1623 :type perm: str
1620 1624
1621 1625 Example output:
1622 1626
1623 1627 .. code-block:: bash
1624 1628
1625 1629 id : <id_given_in_input>
1626 1630 result : {
1627 1631 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1628 1632 "success": true
1629 1633
1630 1634 }
1631 1635 error : null
1632 1636
1633 1637 Example error output:
1634 1638
1635 1639 .. code-block:: bash
1636 1640
1637 1641 id : <id_given_in_input>
1638 1642 result : null
1639 1643 error : {
1640 1644 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1641 1645 }
1642 1646
1643 1647 """
1644 1648
1645 1649 repo = get_repo_or_error(repoid)
1646 1650 perm = get_perm_or_error(perm)
1647 1651 if not has_superadmin_permission(apiuser):
1648 1652 _perms = ('repository.admin',)
1649 1653 validate_repo_permissions(apiuser, repoid, repo, _perms)
1650 1654
1651 1655 user_group = get_user_group_or_error(usergroupid)
1652 1656 if not has_superadmin_permission(apiuser):
1653 1657 # check if we have at least read permission for this user group !
1654 1658 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1655 1659 if not HasUserGroupPermissionAnyApi(*_perms)(
1656 1660 user=apiuser, user_group_name=user_group.users_group_name):
1657 1661 raise JSONRPCError(
1658 1662 'user group `%s` does not exist' % (usergroupid,))
1659 1663
1660 1664 try:
1661 1665 RepoModel().grant_user_group_permission(
1662 1666 repo=repo, group_name=user_group, perm=perm)
1663 1667
1664 1668 Session().commit()
1665 1669 return {
1666 1670 'msg': 'Granted perm: `%s` for user group: `%s` in '
1667 1671 'repo: `%s`' % (
1668 1672 perm.permission_name, user_group.users_group_name,
1669 1673 repo.repo_name
1670 1674 ),
1671 1675 'success': True
1672 1676 }
1673 1677 except Exception:
1674 1678 log.exception(
1675 1679 "Exception occurred while trying change permission on repo")
1676 1680 raise JSONRPCError(
1677 1681 'failed to edit permission for user group: `%s` in '
1678 1682 'repo: `%s`' % (
1679 1683 usergroupid, repo.repo_name
1680 1684 )
1681 1685 )
1682 1686
1683 1687
1684 1688 @jsonrpc_method()
1685 1689 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1686 1690 """
1687 1691 Revoke the permissions of a user group on a given repository.
1688 1692
1689 1693 This command can only be run using an |authtoken| with admin
1690 1694 permissions on the |repo|.
1691 1695
1692 1696 :param apiuser: This is filled automatically from the |authtoken|.
1693 1697 :type apiuser: AuthUser
1694 1698 :param repoid: Set the repository name or repository ID.
1695 1699 :type repoid: str or int
1696 1700 :param usergroupid: Specify the user group ID.
1697 1701 :type usergroupid: str or int
1698 1702
1699 1703 Example output:
1700 1704
1701 1705 .. code-block:: bash
1702 1706
1703 1707 id : <id_given_in_input>
1704 1708 result: {
1705 1709 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1706 1710 "success": true
1707 1711 }
1708 1712 error: null
1709 1713 """
1710 1714
1711 1715 repo = get_repo_or_error(repoid)
1712 1716 if not has_superadmin_permission(apiuser):
1713 1717 _perms = ('repository.admin',)
1714 1718 validate_repo_permissions(apiuser, repoid, repo, _perms)
1715 1719
1716 1720 user_group = get_user_group_or_error(usergroupid)
1717 1721 if not has_superadmin_permission(apiuser):
1718 1722 # check if we have at least read permission for this user group !
1719 1723 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1720 1724 if not HasUserGroupPermissionAnyApi(*_perms)(
1721 1725 user=apiuser, user_group_name=user_group.users_group_name):
1722 1726 raise JSONRPCError(
1723 1727 'user group `%s` does not exist' % (usergroupid,))
1724 1728
1725 1729 try:
1726 1730 RepoModel().revoke_user_group_permission(
1727 1731 repo=repo, group_name=user_group)
1728 1732
1729 1733 Session().commit()
1730 1734 return {
1731 1735 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1732 1736 user_group.users_group_name, repo.repo_name
1733 1737 ),
1734 1738 'success': True
1735 1739 }
1736 1740 except Exception:
1737 1741 log.exception("Exception occurred while trying revoke "
1738 1742 "user group permission on repo")
1739 1743 raise JSONRPCError(
1740 1744 'failed to edit permission for user group: `%s` in '
1741 1745 'repo: `%s`' % (
1742 1746 user_group.users_group_name, repo.repo_name
1743 1747 )
1744 1748 )
1745 1749
1746 1750
1747 1751 @jsonrpc_method()
1748 1752 def pull(request, apiuser, repoid):
1749 1753 """
1750 1754 Triggers a pull on the given repository from a remote location. You
1751 1755 can use this to keep remote repositories up-to-date.
1752 1756
1753 1757 This command can only be run using an |authtoken| with admin
1754 1758 rights to the specified repository. For more information,
1755 1759 see :ref:`config-token-ref`.
1756 1760
1757 1761 This command takes the following options:
1758 1762
1759 1763 :param apiuser: This is filled automatically from the |authtoken|.
1760 1764 :type apiuser: AuthUser
1761 1765 :param repoid: The repository name or repository ID.
1762 1766 :type repoid: str or int
1763 1767
1764 1768 Example output:
1765 1769
1766 1770 .. code-block:: bash
1767 1771
1768 1772 id : <id_given_in_input>
1769 1773 result : {
1770 1774 "msg": "Pulled from `<repository name>`"
1771 1775 "repository": "<repository name>"
1772 1776 }
1773 1777 error : null
1774 1778
1775 1779 Example error output:
1776 1780
1777 1781 .. code-block:: bash
1778 1782
1779 1783 id : <id_given_in_input>
1780 1784 result : null
1781 1785 error : {
1782 1786 "Unable to pull changes from `<reponame>`"
1783 1787 }
1784 1788
1785 1789 """
1786 1790
1787 1791 repo = get_repo_or_error(repoid)
1788 1792 if not has_superadmin_permission(apiuser):
1789 1793 _perms = ('repository.admin',)
1790 1794 validate_repo_permissions(apiuser, repoid, repo, _perms)
1791 1795
1792 1796 try:
1793 1797 ScmModel().pull_changes(repo.repo_name, apiuser.username)
1794 1798 return {
1795 1799 'msg': 'Pulled from `%s`' % repo.repo_name,
1796 1800 'repository': repo.repo_name
1797 1801 }
1798 1802 except Exception:
1799 1803 log.exception("Exception occurred while trying to "
1800 1804 "pull changes from remote location")
1801 1805 raise JSONRPCError(
1802 1806 'Unable to pull changes from `%s`' % repo.repo_name
1803 1807 )
1804 1808
1805 1809
1806 1810 @jsonrpc_method()
1807 1811 def strip(request, apiuser, repoid, revision, branch):
1808 1812 """
1809 1813 Strips the given revision from the specified repository.
1810 1814
1811 1815 * This will remove the revision and all of its decendants.
1812 1816
1813 1817 This command can only be run using an |authtoken| with admin rights to
1814 1818 the specified repository.
1815 1819
1816 1820 This command takes the following options:
1817 1821
1818 1822 :param apiuser: This is filled automatically from the |authtoken|.
1819 1823 :type apiuser: AuthUser
1820 1824 :param repoid: The repository name or repository ID.
1821 1825 :type repoid: str or int
1822 1826 :param revision: The revision you wish to strip.
1823 1827 :type revision: str
1824 1828 :param branch: The branch from which to strip the revision.
1825 1829 :type branch: str
1826 1830
1827 1831 Example output:
1828 1832
1829 1833 .. code-block:: bash
1830 1834
1831 1835 id : <id_given_in_input>
1832 1836 result : {
1833 1837 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
1834 1838 "repository": "<repository name>"
1835 1839 }
1836 1840 error : null
1837 1841
1838 1842 Example error output:
1839 1843
1840 1844 .. code-block:: bash
1841 1845
1842 1846 id : <id_given_in_input>
1843 1847 result : null
1844 1848 error : {
1845 1849 "Unable to strip commit <commit_hash> from repo `<repository name>`"
1846 1850 }
1847 1851
1848 1852 """
1849 1853
1850 1854 repo = get_repo_or_error(repoid)
1851 1855 if not has_superadmin_permission(apiuser):
1852 1856 _perms = ('repository.admin',)
1853 1857 validate_repo_permissions(apiuser, repoid, repo, _perms)
1854 1858
1855 1859 try:
1856 1860 ScmModel().strip(repo, revision, branch)
1857 1861 audit_logger.store_api(
1858 1862 'repo.commit.strip', action_data={'commit_id': revision},
1859 1863 repo=repo,
1860 1864 user=apiuser, commit=True)
1861 1865
1862 1866 return {
1863 1867 'msg': 'Stripped commit %s from repo `%s`' % (
1864 1868 revision, repo.repo_name),
1865 1869 'repository': repo.repo_name
1866 1870 }
1867 1871 except Exception:
1868 1872 log.exception("Exception while trying to strip")
1869 1873 raise JSONRPCError(
1870 1874 'Unable to strip commit %s from repo `%s`' % (
1871 1875 revision, repo.repo_name)
1872 1876 )
1873 1877
1874 1878
1875 1879 @jsonrpc_method()
1876 1880 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
1877 1881 """
1878 1882 Returns all settings for a repository. If key is given it only returns the
1879 1883 setting identified by the key or null.
1880 1884
1881 1885 :param apiuser: This is filled automatically from the |authtoken|.
1882 1886 :type apiuser: AuthUser
1883 1887 :param repoid: The repository name or repository id.
1884 1888 :type repoid: str or int
1885 1889 :param key: Key of the setting to return.
1886 1890 :type: key: Optional(str)
1887 1891
1888 1892 Example output:
1889 1893
1890 1894 .. code-block:: bash
1891 1895
1892 1896 {
1893 1897 "error": null,
1894 1898 "id": 237,
1895 1899 "result": {
1896 1900 "extensions_largefiles": true,
1897 1901 "extensions_evolve": true,
1898 1902 "hooks_changegroup_push_logger": true,
1899 1903 "hooks_changegroup_repo_size": false,
1900 1904 "hooks_outgoing_pull_logger": true,
1901 1905 "phases_publish": "True",
1902 1906 "rhodecode_hg_use_rebase_for_merging": true,
1903 1907 "rhodecode_pr_merge_enabled": true,
1904 1908 "rhodecode_use_outdated_comments": true
1905 1909 }
1906 1910 }
1907 1911 """
1908 1912
1909 1913 # Restrict access to this api method to admins only.
1910 1914 if not has_superadmin_permission(apiuser):
1911 1915 raise JSONRPCForbidden()
1912 1916
1913 1917 try:
1914 1918 repo = get_repo_or_error(repoid)
1915 1919 settings_model = VcsSettingsModel(repo=repo)
1916 1920 settings = settings_model.get_global_settings()
1917 1921 settings.update(settings_model.get_repo_settings())
1918 1922
1919 1923 # If only a single setting is requested fetch it from all settings.
1920 1924 key = Optional.extract(key)
1921 1925 if key is not None:
1922 1926 settings = settings.get(key, None)
1923 1927 except Exception:
1924 1928 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
1925 1929 log.exception(msg)
1926 1930 raise JSONRPCError(msg)
1927 1931
1928 1932 return settings
1929 1933
1930 1934
1931 1935 @jsonrpc_method()
1932 1936 def set_repo_settings(request, apiuser, repoid, settings):
1933 1937 """
1934 1938 Update repository settings. Returns true on success.
1935 1939
1936 1940 :param apiuser: This is filled automatically from the |authtoken|.
1937 1941 :type apiuser: AuthUser
1938 1942 :param repoid: The repository name or repository id.
1939 1943 :type repoid: str or int
1940 1944 :param settings: The new settings for the repository.
1941 1945 :type: settings: dict
1942 1946
1943 1947 Example output:
1944 1948
1945 1949 .. code-block:: bash
1946 1950
1947 1951 {
1948 1952 "error": null,
1949 1953 "id": 237,
1950 1954 "result": true
1951 1955 }
1952 1956 """
1953 1957 # Restrict access to this api method to admins only.
1954 1958 if not has_superadmin_permission(apiuser):
1955 1959 raise JSONRPCForbidden()
1956 1960
1957 1961 if type(settings) is not dict:
1958 1962 raise JSONRPCError('Settings have to be a JSON Object.')
1959 1963
1960 1964 try:
1961 1965 settings_model = VcsSettingsModel(repo=repoid)
1962 1966
1963 1967 # Merge global, repo and incoming settings.
1964 1968 new_settings = settings_model.get_global_settings()
1965 1969 new_settings.update(settings_model.get_repo_settings())
1966 1970 new_settings.update(settings)
1967 1971
1968 1972 # Update the settings.
1969 1973 inherit_global_settings = new_settings.get(
1970 1974 'inherit_global_settings', False)
1971 1975 settings_model.create_or_update_repo_settings(
1972 1976 new_settings, inherit_global_settings=inherit_global_settings)
1973 1977 Session().commit()
1974 1978 except Exception:
1975 1979 msg = 'Failed to update settings for repository `{}`'.format(repoid)
1976 1980 log.exception(msg)
1977 1981 raise JSONRPCError(msg)
1978 1982
1979 1983 # Indicate success.
1980 1984 return True
1981 1985
1982 1986
1983 1987 @jsonrpc_method()
1984 1988 def maintenance(request, apiuser, repoid):
1985 1989 """
1986 1990 Triggers a maintenance on the given repository.
1987 1991
1988 1992 This command can only be run using an |authtoken| with admin
1989 1993 rights to the specified repository. For more information,
1990 1994 see :ref:`config-token-ref`.
1991 1995
1992 1996 This command takes the following options:
1993 1997
1994 1998 :param apiuser: This is filled automatically from the |authtoken|.
1995 1999 :type apiuser: AuthUser
1996 2000 :param repoid: The repository name or repository ID.
1997 2001 :type repoid: str or int
1998 2002
1999 2003 Example output:
2000 2004
2001 2005 .. code-block:: bash
2002 2006
2003 2007 id : <id_given_in_input>
2004 2008 result : {
2005 2009 "msg": "executed maintenance command",
2006 2010 "executed_actions": [
2007 2011 <action_message>, <action_message2>...
2008 2012 ],
2009 2013 "repository": "<repository name>"
2010 2014 }
2011 2015 error : null
2012 2016
2013 2017 Example error output:
2014 2018
2015 2019 .. code-block:: bash
2016 2020
2017 2021 id : <id_given_in_input>
2018 2022 result : null
2019 2023 error : {
2020 2024 "Unable to execute maintenance on `<reponame>`"
2021 2025 }
2022 2026
2023 2027 """
2024 2028
2025 2029 repo = get_repo_or_error(repoid)
2026 2030 if not has_superadmin_permission(apiuser):
2027 2031 _perms = ('repository.admin',)
2028 2032 validate_repo_permissions(apiuser, repoid, repo, _perms)
2029 2033
2030 2034 try:
2031 2035 maintenance = repo_maintenance.RepoMaintenance()
2032 2036 executed_actions = maintenance.execute(repo)
2033 2037
2034 2038 return {
2035 2039 'msg': 'executed maintenance command',
2036 2040 'executed_actions': executed_actions,
2037 2041 'repository': repo.repo_name
2038 2042 }
2039 2043 except Exception:
2040 2044 log.exception("Exception occurred while trying to run maintenance")
2041 2045 raise JSONRPCError(
2042 2046 'Unable to execute maintenance on `%s`' % repo.repo_name)
@@ -1,414 +1,414 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import colander
22 22 import deform.widget
23 23
24 24 from rhodecode.translation import _
25 25 from rhodecode.model.validation_schema.utils import convert_to_optgroup
26 26 from rhodecode.model.validation_schema import validators, preparers, types
27 27
28 28 DEFAULT_LANDING_REF = 'rev:tip'
29 29
30 30
31 31 def get_group_and_repo(repo_name):
32 32 from rhodecode.model.repo_group import RepoGroupModel
33 33 return RepoGroupModel()._get_group_name_and_parent(
34 34 repo_name, get_object=True)
35 35
36 36
37 37 def get_repo_group(repo_group_id):
38 38 from rhodecode.model.repo_group import RepoGroup
39 39 return RepoGroup.get(repo_group_id), RepoGroup.CHOICES_SEPARATOR
40 40
41 41
42 42 @colander.deferred
43 43 def deferred_repo_type_validator(node, kw):
44 44 options = kw.get('repo_type_options', [])
45 45 return colander.OneOf([x for x in options])
46 46
47 47
48 48 @colander.deferred
49 49 def deferred_repo_owner_validator(node, kw):
50 50
51 51 def repo_owner_validator(node, value):
52 52 from rhodecode.model.db import User
53 53 existing = User.get_by_username(value)
54 54 if not existing:
55 55 msg = _(u'Repo owner with id `{}` does not exists').format(value)
56 56 raise colander.Invalid(node, msg)
57 57
58 58 return repo_owner_validator
59 59
60 60
61 61 @colander.deferred
62 62 def deferred_landing_ref_validator(node, kw):
63 63 options = kw.get(
64 64 'repo_ref_options', [DEFAULT_LANDING_REF])
65 65 return colander.OneOf([x for x in options])
66 66
67 67
68 68 @colander.deferred
69 69 def deferred_clone_uri_validator(node, kw):
70 70 repo_type = kw.get('repo_type')
71 71 validator = validators.CloneUriValidator(repo_type)
72 72 return validator
73 73
74 74
75 75 @colander.deferred
76 76 def deferred_landing_ref_widget(node, kw):
77 77 items = kw.get(
78 78 'repo_ref_items', [(DEFAULT_LANDING_REF, DEFAULT_LANDING_REF)])
79 79 items = convert_to_optgroup(items)
80 80 return deform.widget.Select2Widget(values=items)
81 81
82 82
83 83 @colander.deferred
84 84 def deferred_fork_of_validator(node, kw):
85 85 old_values = kw.get('old_values') or {}
86 86
87 87 def fork_of_validator(node, value):
88 88 from rhodecode.model.db import Repository, RepoGroup
89 89 existing = Repository.get_by_repo_name(value)
90 90 if not existing:
91 91 msg = _(u'Fork with id `{}` does not exists').format(value)
92 92 raise colander.Invalid(node, msg)
93 93 elif old_values['repo_name'] == existing.repo_name:
94 94 msg = _(u'Cannot set fork of '
95 95 u'parameter of this repository to itself').format(value)
96 96 raise colander.Invalid(node, msg)
97 97
98 98 return fork_of_validator
99 99
100 100
101 101 @colander.deferred
102 102 def deferred_can_write_to_group_validator(node, kw):
103 103 request_user = kw.get('user')
104 104 old_values = kw.get('old_values') or {}
105 105
106 106 def can_write_to_group_validator(node, value):
107 107 """
108 108 Checks if given repo path is writable by user. This includes checks if
109 109 user is allowed to create repositories under root path or under
110 110 repo group paths
111 111 """
112 112
113 113 from rhodecode.lib.auth import (
114 114 HasPermissionAny, HasRepoGroupPermissionAny)
115 115 from rhodecode.model.repo_group import RepoGroupModel
116 116
117 117 messages = {
118 118 'invalid_repo_group':
119 119 _(u"Repository group `{}` does not exist"),
120 120 # permissions denied we expose as not existing, to prevent
121 121 # resource discovery
122 122 'permission_denied':
123 123 _(u"Repository group `{}` does not exist"),
124 124 'permission_denied_root':
125 125 _(u"You do not have the permission to store "
126 126 u"repositories in the root location.")
127 127 }
128 128
129 129 value = value['repo_group_name']
130 130
131 131 is_root_location = value is types.RootLocation
132 132 # NOT initialized validators, we must call them
133 133 can_create_repos_at_root = HasPermissionAny(
134 134 'hg.admin', 'hg.create.repository')
135 135
136 136 # if values is root location, we simply need to check if we can write
137 137 # to root location !
138 138 if is_root_location:
139 139 if can_create_repos_at_root(user=request_user):
140 140 # we can create repo group inside tool-level. No more checks
141 141 # are required
142 142 return
143 143 else:
144 144 # "fake" node name as repo_name, otherwise we oddly report
145 145 # the error as if it was coming form repo_group
146 146 # however repo_group is empty when using root location.
147 147 node.name = 'repo_name'
148 148 raise colander.Invalid(node, messages['permission_denied_root'])
149 149
150 150 # parent group not exists ? throw an error
151 151 repo_group = RepoGroupModel().get_by_group_name(value)
152 152 if value and not repo_group:
153 153 raise colander.Invalid(
154 154 node, messages['invalid_repo_group'].format(value))
155 155
156 156 gr_name = repo_group.group_name
157 157
158 158 # create repositories with write permission on group is set to true
159 159 create_on_write = HasPermissionAny(
160 160 'hg.create.write_on_repogroup.true')(user=request_user)
161 161
162 162 group_admin = HasRepoGroupPermissionAny('group.admin')(
163 163 gr_name, 'can write into group validator', user=request_user)
164 164 group_write = HasRepoGroupPermissionAny('group.write')(
165 165 gr_name, 'can write into group validator', user=request_user)
166 166
167 167 forbidden = not (group_admin or (group_write and create_on_write))
168 168
169 169 # TODO: handling of old values, and detecting no-change in path
170 170 # to skip permission checks in such cases. This only needs to be
171 171 # implemented if we use this schema in forms as well
172 172
173 173 # gid = (old_data['repo_group'].get('group_id')
174 174 # if (old_data and 'repo_group' in old_data) else None)
175 175 # value_changed = gid != safe_int(value)
176 176 # new = not old_data
177 177
178 178 # do check if we changed the value, there's a case that someone got
179 179 # revoked write permissions to a repository, he still created, we
180 180 # don't need to check permission if he didn't change the value of
181 181 # groups in form box
182 182 # if value_changed or new:
183 183 # # parent group need to be existing
184 184 # TODO: ENDS HERE
185 185
186 186 if repo_group and forbidden:
187 187 msg = messages['permission_denied'].format(value)
188 188 raise colander.Invalid(node, msg)
189 189
190 190 return can_write_to_group_validator
191 191
192 192
193 193 @colander.deferred
194 194 def deferred_unique_name_validator(node, kw):
195 195 request_user = kw.get('user')
196 196 old_values = kw.get('old_values') or {}
197 197
198 198 def unique_name_validator(node, value):
199 199 from rhodecode.model.db import Repository, RepoGroup
200 200 name_changed = value != old_values.get('repo_name')
201 201
202 202 existing = Repository.get_by_repo_name(value)
203 203 if name_changed and existing:
204 204 msg = _(u'Repository with name `{}` already exists').format(value)
205 205 raise colander.Invalid(node, msg)
206 206
207 207 existing_group = RepoGroup.get_by_group_name(value)
208 208 if name_changed and existing_group:
209 209 msg = _(u'Repository group with name `{}` already exists').format(
210 210 value)
211 211 raise colander.Invalid(node, msg)
212 212 return unique_name_validator
213 213
214 214
215 215 @colander.deferred
216 216 def deferred_repo_name_validator(node, kw):
217 217 def no_git_suffix_validator(node, value):
218 218 if value.endswith('.git'):
219 219 msg = _('Repository name cannot end with .git')
220 220 raise colander.Invalid(node, msg)
221 221 return colander.All(
222 222 no_git_suffix_validator, validators.valid_name_validator)
223 223
224 224
225 225 @colander.deferred
226 226 def deferred_repo_group_validator(node, kw):
227 227 options = kw.get(
228 228 'repo_repo_group_options')
229 229 return colander.OneOf([x for x in options])
230 230
231 231
232 232 @colander.deferred
233 233 def deferred_repo_group_widget(node, kw):
234 234 items = kw.get('repo_repo_group_items')
235 235 return deform.widget.Select2Widget(values=items)
236 236
237 237
238 238 class GroupType(colander.Mapping):
239 239 def _validate(self, node, value):
240 240 try:
241 241 return dict(repo_group_name=value)
242 242 except Exception as e:
243 243 raise colander.Invalid(
244 244 node, '"${val}" is not a mapping type: ${err}'.format(
245 245 val=value, err=e))
246 246
247 247 def deserialize(self, node, cstruct):
248 248 if cstruct is colander.null:
249 249 return cstruct
250 250
251 251 appstruct = super(GroupType, self).deserialize(node, cstruct)
252 252 validated_name = appstruct['repo_group_name']
253 253
254 254 # inject group based on once deserialized data
255 255 (repo_name_without_group,
256 256 parent_group_name,
257 257 parent_group) = get_group_and_repo(validated_name)
258 258
259 259 appstruct['repo_name_with_group'] = validated_name
260 260 appstruct['repo_name_without_group'] = repo_name_without_group
261 261 appstruct['repo_group_name'] = parent_group_name or types.RootLocation
262 262
263 263 if parent_group:
264 264 appstruct['repo_group_id'] = parent_group.group_id
265 265
266 266 return appstruct
267 267
268 268
269 269 class GroupSchema(colander.SchemaNode):
270 270 schema_type = GroupType
271 271 validator = deferred_can_write_to_group_validator
272 272 missing = colander.null
273 273
274 274
275 275 class RepoGroup(GroupSchema):
276 276 repo_group_name = colander.SchemaNode(
277 277 types.GroupNameType())
278 278 repo_group_id = colander.SchemaNode(
279 279 colander.String(), missing=None)
280 280 repo_name_without_group = colander.SchemaNode(
281 281 colander.String(), missing=None)
282 282
283 283
284 284 class RepoGroupAccessSchema(colander.MappingSchema):
285 285 repo_group = RepoGroup()
286 286
287 287
288 288 class RepoNameUniqueSchema(colander.MappingSchema):
289 289 unique_repo_name = colander.SchemaNode(
290 290 colander.String(),
291 291 validator=deferred_unique_name_validator)
292 292
293 293
294 294 class RepoSchema(colander.MappingSchema):
295 295
296 296 repo_name = colander.SchemaNode(
297 297 types.RepoNameType(),
298 298 validator=deferred_repo_name_validator)
299 299
300 300 repo_type = colander.SchemaNode(
301 301 colander.String(),
302 302 validator=deferred_repo_type_validator)
303 303
304 304 repo_owner = colander.SchemaNode(
305 305 colander.String(),
306 306 validator=deferred_repo_owner_validator,
307 307 widget=deform.widget.TextInputWidget())
308 308
309 309 repo_description = colander.SchemaNode(
310 310 colander.String(), missing='',
311 311 widget=deform.widget.TextAreaWidget())
312 312
313 313 repo_landing_commit_ref = colander.SchemaNode(
314 314 colander.String(),
315 315 validator=deferred_landing_ref_validator,
316 316 preparers=[preparers.strip_preparer],
317 317 missing=DEFAULT_LANDING_REF,
318 318 widget=deferred_landing_ref_widget)
319 319
320 320 repo_clone_uri = colander.SchemaNode(
321 321 colander.String(),
322 validator=colander.All(colander.Length(min=1)),
322 validator=deferred_clone_uri_validator,
323 323 preparers=[preparers.strip_preparer],
324 324 missing='')
325 325
326 326 repo_fork_of = colander.SchemaNode(
327 327 colander.String(),
328 328 validator=deferred_fork_of_validator,
329 329 missing=None)
330 330
331 331 repo_private = colander.SchemaNode(
332 332 types.StringBooleanType(),
333 333 missing=False, widget=deform.widget.CheckboxWidget())
334 334 repo_copy_permissions = colander.SchemaNode(
335 335 types.StringBooleanType(),
336 336 missing=False, widget=deform.widget.CheckboxWidget())
337 337 repo_enable_statistics = colander.SchemaNode(
338 338 types.StringBooleanType(),
339 339 missing=False, widget=deform.widget.CheckboxWidget())
340 340 repo_enable_downloads = colander.SchemaNode(
341 341 types.StringBooleanType(),
342 342 missing=False, widget=deform.widget.CheckboxWidget())
343 343 repo_enable_locking = colander.SchemaNode(
344 344 types.StringBooleanType(),
345 345 missing=False, widget=deform.widget.CheckboxWidget())
346 346
347 347 def deserialize(self, cstruct):
348 348 """
349 349 Custom deserialize that allows to chain validation, and verify
350 350 permissions, and as last step uniqueness
351 351 """
352 352
353 353 # first pass, to validate given data
354 354 appstruct = super(RepoSchema, self).deserialize(cstruct)
355 355 validated_name = appstruct['repo_name']
356 356
357 357 # second pass to validate permissions to repo_group
358 358 second = RepoGroupAccessSchema().bind(**self.bindings)
359 359 appstruct_second = second.deserialize({'repo_group': validated_name})
360 360 # save result
361 361 appstruct['repo_group'] = appstruct_second['repo_group']
362 362
363 363 # thirds to validate uniqueness
364 364 third = RepoNameUniqueSchema().bind(**self.bindings)
365 365 third.deserialize({'unique_repo_name': validated_name})
366 366
367 367 return appstruct
368 368
369 369
370 370 class RepoSettingsSchema(RepoSchema):
371 371 repo_group = colander.SchemaNode(
372 372 colander.Integer(),
373 373 validator=deferred_repo_group_validator,
374 374 widget=deferred_repo_group_widget,
375 375 missing='')
376 376
377 377 repo_clone_uri_change = colander.SchemaNode(
378 378 colander.String(),
379 379 missing='NEW')
380 380
381 381 repo_clone_uri = colander.SchemaNode(
382 382 colander.String(),
383 383 preparers=[preparers.strip_preparer],
384 384 validator=deferred_clone_uri_validator,
385 385 missing='')
386 386
387 387 def deserialize(self, cstruct):
388 388 """
389 389 Custom deserialize that allows to chain validation, and verify
390 390 permissions, and as last step uniqueness
391 391 """
392 392
393 393 # first pass, to validate given data
394 394 appstruct = super(RepoSchema, self).deserialize(cstruct)
395 395 validated_name = appstruct['repo_name']
396 396 # because of repoSchema adds repo-group as an ID, we inject it as
397 397 # full name here because validators require it, it's unwrapped later
398 398 # so it's safe to use and final name is going to be without group anyway
399 399
400 400 group, separator = get_repo_group(appstruct['repo_group'])
401 401 if group:
402 402 validated_name = separator.join([group.group_name, validated_name])
403 403
404 404 # second pass to validate permissions to repo_group
405 405 second = RepoGroupAccessSchema().bind(**self.bindings)
406 406 appstruct_second = second.deserialize({'repo_group': validated_name})
407 407 # save result
408 408 appstruct['repo_group'] = appstruct_second['repo_group']
409 409
410 410 # thirds to validate uniqueness
411 411 third = RepoNameUniqueSchema().bind(**self.bindings)
412 412 third.deserialize({'unique_repo_name': validated_name})
413 413
414 414 return appstruct
@@ -1,149 +1,152 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import re
23 23 import logging
24 24
25 25
26 26 import ipaddress
27 27 import colander
28 28
29 29 from rhodecode.translation import _
30 30 from rhodecode.lib.utils2 import glob2re, safe_unicode
31 31 from rhodecode.lib.ext_json import json
32 32
33 33 log = logging.getLogger(__name__)
34 34
35 35
36 36 def ip_addr_validator(node, value):
37 37 try:
38 38 # this raises an ValueError if address is not IpV4 or IpV6
39 39 ipaddress.ip_network(safe_unicode(value), strict=False)
40 40 except ValueError:
41 41 msg = _(u'Please enter a valid IPv4 or IpV6 address')
42 42 raise colander.Invalid(node, msg)
43 43
44 44
45 45 class IpAddrValidator(object):
46 46 def __init__(self, strict=True):
47 47 self.strict = strict
48 48
49 49 def __call__(self, node, value):
50 50 try:
51 51 # this raises an ValueError if address is not IpV4 or IpV6
52 52 ipaddress.ip_network(safe_unicode(value), strict=self.strict)
53 53 except ValueError:
54 54 msg = _(u'Please enter a valid IPv4 or IpV6 address')
55 55 raise colander.Invalid(node, msg)
56 56
57 57
58 58 def glob_validator(node, value):
59 59 try:
60 60 re.compile('^' + glob2re(value) + '$')
61 61 except Exception:
62 62 msg = _(u'Invalid glob pattern')
63 63 raise colander.Invalid(node, msg)
64 64
65 65
66 66 def valid_name_validator(node, value):
67 67 from rhodecode.model.validation_schema import types
68 68 if value is types.RootLocation:
69 69 return
70 70
71 71 msg = _('Name must start with a letter or number. Got `{}`').format(value)
72 72 if not re.match(r'^[a-zA-z0-9]{1,}', value):
73 73 raise colander.Invalid(node, msg)
74 74
75 75
76 76 class InvalidCloneUrl(Exception):
77 77 allowed_prefixes = ()
78 78
79 79
80 80 def url_validator(url, repo_type, config):
81 81 from rhodecode.lib.vcs.backends.hg import MercurialRepository
82 82 from rhodecode.lib.vcs.backends.git import GitRepository
83 83 from rhodecode.lib.vcs.backends.svn import SubversionRepository
84 84
85 85 if repo_type == 'hg':
86 86 allowed_prefixes = ('http', 'svn+http', 'git+http')
87 87
88 88 if 'http' in url[:4]:
89 89 # initially check if it's at least the proper URL
90 90 # or does it pass basic auth
91 91
92 92 MercurialRepository.check_url(url, config)
93 93 elif 'svn+http' in url[:8]: # svn->hg import
94 94 SubversionRepository.check_url(url, config)
95 95 elif 'git+http' in url[:8]: # git->hg import
96 96 raise NotImplementedError()
97 97 else:
98 98 exc = InvalidCloneUrl('Clone from URI %s not allowed. '
99 99 'Allowed url must start with one of %s'
100 100 % (url, ','.join(allowed_prefixes)))
101 101 exc.allowed_prefixes = allowed_prefixes
102 102 raise exc
103 103
104 104 elif repo_type == 'git':
105 105 allowed_prefixes = ('http', 'svn+http', 'hg+http')
106 106 if 'http' in url[:4]:
107 107 # initially check if it's at least the proper URL
108 108 # or does it pass basic auth
109 109 GitRepository.check_url(url, config)
110 110 elif 'svn+http' in url[:8]: # svn->git import
111 111 raise NotImplementedError()
112 112 elif 'hg+http' in url[:8]: # hg->git import
113 113 raise NotImplementedError()
114 114 else:
115 115 exc = InvalidCloneUrl('Clone from URI %s not allowed. '
116 116 'Allowed url must start with one of %s'
117 117 % (url, ','.join(allowed_prefixes)))
118 118 exc.allowed_prefixes = allowed_prefixes
119 119 raise exc
120 elif repo_type == 'svn':
121 # no validation for SVN yet
122 return
123
124 raise InvalidCloneUrl('No repo type specified')
120 125
121 126
122 127 class CloneUriValidator(object):
123 128 def __init__(self, repo_type):
124 129 self.repo_type = repo_type
125 130
126 131 def __call__(self, node, value):
132
127 133 from rhodecode.lib.utils import make_db_config
128 134 try:
129 135 config = make_db_config(clear_session=False)
130 136 url_validator(value, self.repo_type, config)
131 137 except InvalidCloneUrl as e:
132 138 log.warning(e)
133 msg = _(u'Invalid clone url, provide a valid clone '
134 u'url starting with one of {allowed_prefixes}').format(
135 allowed_prefixes=e.allowed_prefixes)
136 raise colander.Invalid(node, msg)
139 raise colander.Invalid(node, e.message)
137 140 except Exception:
138 141 log.exception('Url validation failed')
139 142 msg = _(u'invalid clone url for {repo_type} repository').format(
140 143 repo_type=self.repo_type)
141 144 raise colander.Invalid(node, msg)
142 145
143 146
144 147 def json_validator(node, value):
145 148 try:
146 149 json.loads(value)
147 150 except (Exception,):
148 151 msg = _(u'Please enter a valid json object')
149 152 raise colander.Invalid(node, msg)
@@ -1,130 +1,134 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import colander
22 22 import pytest
23 23
24 24 from rhodecode.model.validation_schema import types
25 25 from rhodecode.model.validation_schema.schemas import repo_schema
26 26
27 27
28 28 class TestRepoSchema(object):
29 29
30 30 #TODO:
31 31 # test nested groups
32 32
33 33 @pytest.mark.parametrize('given, expected', [
34 34 ('my repo', 'my-repo'),
35 35 (' hello world mike ', 'hello-world-mike'),
36 36
37 37 ('//group1/group2//', 'group1/group2'),
38 38 ('//group1///group2//', 'group1/group2'),
39 39 ('///group1/group2///group3', 'group1/group2/group3'),
40 40 ('word g1/group2///group3', 'word-g1/group2/group3'),
41 41
42 42 ('grou p1/gro;,,##up2//.../group3', 'grou-p1/group2/group3'),
43 43
44 44 ('group,,,/,,,/1/2/3', 'group/1/2/3'),
45 45 ('grou[]p1/gro;up2///gro up3', 'group1/group2/gro-up3'),
46 46 (u'grou[]p1/gro;up2///gro up3/Δ…Δ‡', u'group1/group2/gro-up3/Δ…Δ‡'),
47 47 ])
48 48 def test_deserialize_repo_name(self, app, user_admin, given, expected):
49 49
50 50 schema = repo_schema.RepoSchema().bind()
51 51 assert expected == schema.get('repo_name').deserialize(given)
52 52
53 53 def test_deserialize(self, app, user_admin):
54 54 schema = repo_schema.RepoSchema().bind(
55 55 repo_type_options=['hg'],
56 repo_type='hg',
56 57 user=user_admin
57 58 )
58 59
59 60 schema_data = schema.deserialize(dict(
60 61 repo_name='my_schema_repo',
61 62 repo_type='hg',
62 63 repo_owner=user_admin.username
63 64 ))
64 65
65 66 assert schema_data['repo_name'] == u'my_schema_repo'
66 67 assert schema_data['repo_group'] == {
67 68 'repo_group_id': None,
68 69 'repo_group_name': types.RootLocation,
69 70 'repo_name_with_group': u'my_schema_repo',
70 71 'repo_name_without_group': u'my_schema_repo'}
71 72
72 73 @pytest.mark.parametrize('given, err_key, expected_exc', [
73 74 ('xxx/my_schema_repo','repo_group', 'Repository group `xxx` does not exist'),
74 75 ('', 'repo_name', 'Name must start with a letter or number. Got ``'),
75 76 ])
76 77 def test_deserialize_with_bad_group_name(
77 78 self, app, user_admin, given, err_key, expected_exc):
78 79
79 80 schema = repo_schema.RepoSchema().bind(
80 81 repo_type_options=['hg'],
82 repo_type='hg',
81 83 user=user_admin
82 84 )
83 85
84 86 with pytest.raises(colander.Invalid) as excinfo:
85 87 schema.deserialize(dict(
86 88 repo_name=given,
87 89 repo_type='hg',
88 90 repo_owner=user_admin.username
89 91 ))
90 92
91 93 assert excinfo.value.asdict()[err_key] == expected_exc
92 94
93 95 def test_deserialize_with_group_name(self, app, user_admin, test_repo_group):
94 96 schema = repo_schema.RepoSchema().bind(
95 97 repo_type_options=['hg'],
98 repo_type='hg',
96 99 user=user_admin
97 100 )
98 101
99 102 full_name = test_repo_group.group_name + u'/my_schema_repo'
100 103 schema_data = schema.deserialize(dict(
101 104 repo_name=full_name,
102 105 repo_type='hg',
103 106 repo_owner=user_admin.username
104 107 ))
105 108
106 109 assert schema_data['repo_name'] == full_name
107 110 assert schema_data['repo_group'] == {
108 111 'repo_group_id': test_repo_group.group_id,
109 112 'repo_group_name': test_repo_group.group_name,
110 113 'repo_name_with_group': full_name,
111 114 'repo_name_without_group': u'my_schema_repo'}
112 115
113 116 def test_deserialize_with_group_name_regular_user_no_perms(
114 117 self, app, user_regular, test_repo_group):
115 118 schema = repo_schema.RepoSchema().bind(
116 119 repo_type_options=['hg'],
120 repo_type='hg',
117 121 user=user_regular
118 122 )
119 123
120 124 full_name = test_repo_group.group_name + '/my_schema_repo'
121 125 with pytest.raises(colander.Invalid) as excinfo:
122 126 schema.deserialize(dict(
123 127 repo_name=full_name,
124 128 repo_type='hg',
125 129 repo_owner=user_regular.username
126 130 ))
127 131
128 132 expected = 'Repository group `{}` does not exist'.format(
129 133 test_repo_group.group_name)
130 134 assert excinfo.value.asdict()['repo_group'] == expected
General Comments 0
You need to be logged in to leave comments. Login now