##// END OF EJS Templates
api: security, fix problem when absolute paths are specified with API call, that would allow...
marcink -
r2663:0777b16f default
parent child Browse files
Show More
@@ -1,195 +1,197 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 ({'push_uri': ''},
60 60 {'push_uri': ''}),
61 61
62 62 ({'landing_rev': 'rev:tip'},
63 63 {'landing_rev': ['rev', 'tip']}),
64 64
65 65 ({'enable_statistics': True},
66 66 SAME_AS_UPDATES),
67 67
68 68 ({'enable_locking': True},
69 69 SAME_AS_UPDATES),
70 70
71 71 ({'enable_downloads': True},
72 72 SAME_AS_UPDATES),
73 73
74 74 ({'repo_name': 'new_repo_name'},
75 75 {
76 76 'repo_name': 'new_repo_name',
77 77 'url': 'http://{}/new_repo_name'.format(http_host_only_stub())
78 78 }),
79 79
80 80 ({'repo_name': 'test_group_for_update/{}'.format(UPDATE_REPO_NAME),
81 81 '_group': 'test_group_for_update'},
82 82 {
83 83 'repo_name': 'test_group_for_update/{}'.format(UPDATE_REPO_NAME),
84 84 'url': 'http://{}/test_group_for_update/{}'.format(
85 85 http_host_only_stub(), UPDATE_REPO_NAME)
86 86 }),
87 87 ])
88 88 def test_api_update_repo(self, updates, expected, backend):
89 89 repo_name = UPDATE_REPO_NAME
90 90 repo = fixture.create_repo(repo_name, repo_type=backend.alias)
91 91 if updates.get('_group'):
92 92 fixture.create_repo_group(updates['_group'])
93 93
94 94 expected_api_data = repo.get_api_data(include_secrets=True)
95 95 if expected is SAME_AS_UPDATES:
96 96 expected_api_data.update(updates)
97 97 else:
98 98 expected_api_data.update(expected)
99 99
100 100 id_, params = build_data(
101 101 self.apikey, 'update_repo', repoid=repo_name, **updates)
102 response = api_call(self.app, params)
102
103 with mock.patch('rhodecode.model.validation_schema.validators.url_validator'):
104 response = api_call(self.app, params)
103 105
104 106 if updates.get('repo_name'):
105 107 repo_name = updates['repo_name']
106 108
107 109 try:
108 110 expected = {
109 111 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo_name),
110 112 'repository': jsonify(expected_api_data)
111 113 }
112 114 assert_ok(id_, expected, given=response.body)
113 115 finally:
114 116 fixture.destroy_repo(repo_name)
115 117 if updates.get('_group'):
116 118 fixture.destroy_repo_group(updates['_group'])
117 119
118 120 def test_api_update_repo_fork_of_field(self, backend):
119 121 master_repo = backend.create_repo()
120 122 repo = backend.create_repo()
121 123 updates = {
122 124 'fork_of': master_repo.repo_name,
123 125 'fork_of_id': master_repo.repo_id
124 126 }
125 127 expected_api_data = repo.get_api_data(include_secrets=True)
126 128 expected_api_data.update(updates)
127 129
128 130 id_, params = build_data(
129 131 self.apikey, 'update_repo', repoid=repo.repo_name, **updates)
130 132 response = api_call(self.app, params)
131 133 expected = {
132 134 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
133 135 'repository': jsonify(expected_api_data)
134 136 }
135 137 assert_ok(id_, expected, given=response.body)
136 138 result = response.json['result']['repository']
137 139 assert result['fork_of'] == master_repo.repo_name
138 140 assert result['fork_of_id'] == master_repo.repo_id
139 141
140 142 def test_api_update_repo_fork_of_not_found(self, backend):
141 143 master_repo_name = 'fake-parent-repo'
142 144 repo = backend.create_repo()
143 145 updates = {
144 146 'fork_of': master_repo_name
145 147 }
146 148 id_, params = build_data(
147 149 self.apikey, 'update_repo', repoid=repo.repo_name, **updates)
148 150 response = api_call(self.app, params)
149 151 expected = {
150 152 'repo_fork_of': 'Fork with id `{}` does not exists'.format(
151 153 master_repo_name)}
152 154 assert_error(id_, expected, given=response.body)
153 155
154 156 def test_api_update_repo_with_repo_group_not_existing(self):
155 157 repo_name = 'admin_owned'
156 158 fake_repo_group = 'test_group_for_update'
157 159 fixture.create_repo(repo_name)
158 160 updates = {'repo_name': '{}/{}'.format(fake_repo_group, repo_name)}
159 161 id_, params = build_data(
160 162 self.apikey, 'update_repo', repoid=repo_name, **updates)
161 163 response = api_call(self.app, params)
162 164 try:
163 165 expected = {
164 166 'repo_group': 'Repository group `{}` does not exist'.format(fake_repo_group)
165 167 }
166 168 assert_error(id_, expected, given=response.body)
167 169 finally:
168 170 fixture.destroy_repo(repo_name)
169 171
170 172 def test_api_update_repo_regular_user_not_allowed(self):
171 173 repo_name = 'admin_owned'
172 174 fixture.create_repo(repo_name)
173 175 updates = {'active': False}
174 176 id_, params = build_data(
175 177 self.apikey_regular, 'update_repo', repoid=repo_name, **updates)
176 178 response = api_call(self.app, params)
177 179 try:
178 180 expected = 'repository `%s` does not exist' % (repo_name,)
179 181 assert_error(id_, expected, given=response.body)
180 182 finally:
181 183 fixture.destroy_repo(repo_name)
182 184
183 185 @mock.patch.object(RepoModel, 'update', crash)
184 186 def test_api_update_repo_exception_occurred(self, backend):
185 187 repo_name = UPDATE_REPO_NAME
186 188 fixture.create_repo(repo_name, repo_type=backend.alias)
187 189 id_, params = build_data(
188 190 self.apikey, 'update_repo', repoid=repo_name,
189 191 owner=TEST_USER_ADMIN_LOGIN,)
190 192 response = api_call(self.app, params)
191 193 try:
192 194 expected = 'failed to update repo `%s`' % (repo_name,)
193 195 assert_error(id_, expected, given=response.body)
194 196 finally:
195 197 fixture.destroy_repo(repo_name)
@@ -1,2060 +1,2064 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 push_uri=Optional(None),
567 567 landing_rev=Optional('rev:tip'),
568 568 enable_statistics=Optional(False),
569 569 enable_locking=Optional(False),
570 570 enable_downloads=Optional(False),
571 571 copy_permissions=Optional(False)):
572 572 """
573 573 Creates a repository.
574 574
575 575 * If the repository name contains "/", repository will be created inside
576 576 a repository group or nested repository groups
577 577
578 578 For example "foo/bar/repo1" will create |repo| called "repo1" inside
579 579 group "foo/bar". You have to have permissions to access and write to
580 580 the last repository group ("bar" in this example)
581 581
582 582 This command can only be run using an |authtoken| with at least
583 583 permissions to create repositories, or write permissions to
584 584 parent repository groups.
585 585
586 586 :param apiuser: This is filled automatically from the |authtoken|.
587 587 :type apiuser: AuthUser
588 588 :param repo_name: Set the repository name.
589 589 :type repo_name: str
590 590 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
591 591 :type repo_type: str
592 592 :param owner: user_id or username
593 593 :type owner: Optional(str)
594 594 :param description: Set the repository description.
595 595 :type description: Optional(str)
596 596 :param private: set repository as private
597 597 :type private: bool
598 598 :param clone_uri: set clone_uri
599 599 :type clone_uri: str
600 600 :param push_uri: set push_uri
601 601 :type push_uri: str
602 602 :param landing_rev: <rev_type>:<rev>
603 603 :type landing_rev: str
604 604 :param enable_locking:
605 605 :type enable_locking: bool
606 606 :param enable_downloads:
607 607 :type enable_downloads: bool
608 608 :param enable_statistics:
609 609 :type enable_statistics: bool
610 610 :param copy_permissions: Copy permission from group in which the
611 611 repository is being created.
612 612 :type copy_permissions: bool
613 613
614 614
615 615 Example output:
616 616
617 617 .. code-block:: bash
618 618
619 619 id : <id_given_in_input>
620 620 result: {
621 621 "msg": "Created new repository `<reponame>`",
622 622 "success": true,
623 623 "task": "<celery task id or None if done sync>"
624 624 }
625 625 error: null
626 626
627 627
628 628 Example error output:
629 629
630 630 .. code-block:: bash
631 631
632 632 id : <id_given_in_input>
633 633 result : null
634 634 error : {
635 635 'failed to create repository `<repo_name>`'
636 636 }
637 637
638 638 """
639 639
640 640 owner = validate_set_owner_permissions(apiuser, owner)
641 641
642 642 description = Optional.extract(description)
643 643 copy_permissions = Optional.extract(copy_permissions)
644 644 clone_uri = Optional.extract(clone_uri)
645 645 push_uri = Optional.extract(push_uri)
646 646 landing_commit_ref = Optional.extract(landing_rev)
647 647
648 648 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
649 649 if isinstance(private, Optional):
650 650 private = defs.get('repo_private') or Optional.extract(private)
651 651 if isinstance(repo_type, Optional):
652 652 repo_type = defs.get('repo_type')
653 653 if isinstance(enable_statistics, Optional):
654 654 enable_statistics = defs.get('repo_enable_statistics')
655 655 if isinstance(enable_locking, Optional):
656 656 enable_locking = defs.get('repo_enable_locking')
657 657 if isinstance(enable_downloads, Optional):
658 658 enable_downloads = defs.get('repo_enable_downloads')
659 659
660 660 schema = repo_schema.RepoSchema().bind(
661 661 repo_type_options=rhodecode.BACKENDS.keys(),
662 repo_type=repo_type,
662 663 # user caller
663 664 user=apiuser)
664 665
665 666 try:
666 667 schema_data = schema.deserialize(dict(
667 668 repo_name=repo_name,
668 669 repo_type=repo_type,
669 670 repo_owner=owner.username,
670 671 repo_description=description,
671 672 repo_landing_commit_ref=landing_commit_ref,
672 673 repo_clone_uri=clone_uri,
673 674 repo_push_uri=push_uri,
674 675 repo_private=private,
675 676 repo_copy_permissions=copy_permissions,
676 677 repo_enable_statistics=enable_statistics,
677 678 repo_enable_downloads=enable_downloads,
678 679 repo_enable_locking=enable_locking))
679 680 except validation_schema.Invalid as err:
680 681 raise JSONRPCValidationError(colander_exc=err)
681 682
682 683 try:
683 684 data = {
684 685 'owner': owner,
685 686 'repo_name': schema_data['repo_group']['repo_name_without_group'],
686 687 'repo_name_full': schema_data['repo_name'],
687 688 'repo_group': schema_data['repo_group']['repo_group_id'],
688 689 'repo_type': schema_data['repo_type'],
689 690 'repo_description': schema_data['repo_description'],
690 691 'repo_private': schema_data['repo_private'],
691 692 'clone_uri': schema_data['repo_clone_uri'],
692 693 'push_uri': schema_data['repo_push_uri'],
693 694 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
694 695 'enable_statistics': schema_data['repo_enable_statistics'],
695 696 'enable_locking': schema_data['repo_enable_locking'],
696 697 'enable_downloads': schema_data['repo_enable_downloads'],
697 698 'repo_copy_permissions': schema_data['repo_copy_permissions'],
698 699 }
699 700
700 701 task = RepoModel().create(form_data=data, cur_user=owner)
701 702 task_id = get_task_id(task)
702 703 # no commit, it's done in RepoModel, or async via celery
703 704 return {
704 705 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
705 706 'success': True, # cannot return the repo data here since fork
706 707 # can be done async
707 708 'task': task_id
708 709 }
709 710 except Exception:
710 711 log.exception(
711 712 u"Exception while trying to create the repository %s",
712 713 schema_data['repo_name'])
713 714 raise JSONRPCError(
714 715 'failed to create repository `%s`' % (schema_data['repo_name'],))
715 716
716 717
717 718 @jsonrpc_method()
718 719 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
719 720 description=Optional('')):
720 721 """
721 722 Adds an extra field to a repository.
722 723
723 724 This command can only be run using an |authtoken| with at least
724 725 write permissions to the |repo|.
725 726
726 727 :param apiuser: This is filled automatically from the |authtoken|.
727 728 :type apiuser: AuthUser
728 729 :param repoid: Set the repository name or repository id.
729 730 :type repoid: str or int
730 731 :param key: Create a unique field key for this repository.
731 732 :type key: str
732 733 :param label:
733 734 :type label: Optional(str)
734 735 :param description:
735 736 :type description: Optional(str)
736 737 """
737 738 repo = get_repo_or_error(repoid)
738 739 if not has_superadmin_permission(apiuser):
739 740 _perms = ('repository.admin',)
740 741 validate_repo_permissions(apiuser, repoid, repo, _perms)
741 742
742 743 label = Optional.extract(label) or key
743 744 description = Optional.extract(description)
744 745
745 746 field = RepositoryField.get_by_key_name(key, repo)
746 747 if field:
747 748 raise JSONRPCError('Field with key '
748 749 '`%s` exists for repo `%s`' % (key, repoid))
749 750
750 751 try:
751 752 RepoModel().add_repo_field(repo, key, field_label=label,
752 753 field_desc=description)
753 754 Session().commit()
754 755 return {
755 756 'msg': "Added new repository field `%s`" % (key,),
756 757 'success': True,
757 758 }
758 759 except Exception:
759 760 log.exception("Exception occurred while trying to add field to repo")
760 761 raise JSONRPCError(
761 762 'failed to create new field for repository `%s`' % (repoid,))
762 763
763 764
764 765 @jsonrpc_method()
765 766 def remove_field_from_repo(request, apiuser, repoid, key):
766 767 """
767 768 Removes an extra field from a repository.
768 769
769 770 This command can only be run using an |authtoken| with at least
770 771 write permissions to the |repo|.
771 772
772 773 :param apiuser: This is filled automatically from the |authtoken|.
773 774 :type apiuser: AuthUser
774 775 :param repoid: Set the repository name or repository ID.
775 776 :type repoid: str or int
776 777 :param key: Set the unique field key for this repository.
777 778 :type key: str
778 779 """
779 780
780 781 repo = get_repo_or_error(repoid)
781 782 if not has_superadmin_permission(apiuser):
782 783 _perms = ('repository.admin',)
783 784 validate_repo_permissions(apiuser, repoid, repo, _perms)
784 785
785 786 field = RepositoryField.get_by_key_name(key, repo)
786 787 if not field:
787 788 raise JSONRPCError('Field with key `%s` does not '
788 789 'exists for repo `%s`' % (key, repoid))
789 790
790 791 try:
791 792 RepoModel().delete_repo_field(repo, field_key=key)
792 793 Session().commit()
793 794 return {
794 795 'msg': "Deleted repository field `%s`" % (key,),
795 796 'success': True,
796 797 }
797 798 except Exception:
798 799 log.exception(
799 800 "Exception occurred while trying to delete field from repo")
800 801 raise JSONRPCError(
801 802 'failed to delete field for repository `%s`' % (repoid,))
802 803
803 804
804 805 @jsonrpc_method()
805 806 def update_repo(
806 807 request, apiuser, repoid, repo_name=Optional(None),
807 808 owner=Optional(OAttr('apiuser')), description=Optional(''),
808 809 private=Optional(False),
809 810 clone_uri=Optional(None), push_uri=Optional(None),
810 811 landing_rev=Optional('rev:tip'), fork_of=Optional(None),
811 812 enable_statistics=Optional(False),
812 813 enable_locking=Optional(False),
813 814 enable_downloads=Optional(False), fields=Optional('')):
814 815 """
815 816 Updates a repository with the given information.
816 817
817 818 This command can only be run using an |authtoken| with at least
818 819 admin permissions to the |repo|.
819 820
820 821 * If the repository name contains "/", repository will be updated
821 822 accordingly with a repository group or nested repository groups
822 823
823 824 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
824 825 called "repo-test" and place it inside group "foo/bar".
825 826 You have to have permissions to access and write to the last repository
826 827 group ("bar" in this example)
827 828
828 829 :param apiuser: This is filled automatically from the |authtoken|.
829 830 :type apiuser: AuthUser
830 831 :param repoid: repository name or repository ID.
831 832 :type repoid: str or int
832 833 :param repo_name: Update the |repo| name, including the
833 834 repository group it's in.
834 835 :type repo_name: str
835 836 :param owner: Set the |repo| owner.
836 837 :type owner: str
837 838 :param fork_of: Set the |repo| as fork of another |repo|.
838 839 :type fork_of: str
839 840 :param description: Update the |repo| description.
840 841 :type description: str
841 842 :param private: Set the |repo| as private. (True | False)
842 843 :type private: bool
843 844 :param clone_uri: Update the |repo| clone URI.
844 845 :type clone_uri: str
845 846 :param landing_rev: Set the |repo| landing revision. Default is ``rev:tip``.
846 847 :type landing_rev: str
847 848 :param enable_statistics: Enable statistics on the |repo|, (True | False).
848 849 :type enable_statistics: bool
849 850 :param enable_locking: Enable |repo| locking.
850 851 :type enable_locking: bool
851 852 :param enable_downloads: Enable downloads from the |repo|, (True | False).
852 853 :type enable_downloads: bool
853 854 :param fields: Add extra fields to the |repo|. Use the following
854 855 example format: ``field_key=field_val,field_key2=fieldval2``.
855 856 Escape ', ' with \,
856 857 :type fields: str
857 858 """
858 859
859 860 repo = get_repo_or_error(repoid)
860 861
861 862 include_secrets = False
862 863 if not has_superadmin_permission(apiuser):
863 864 validate_repo_permissions(apiuser, repoid, repo, ('repository.admin',))
864 865 else:
865 866 include_secrets = True
866 867
867 868 updates = dict(
868 869 repo_name=repo_name
869 870 if not isinstance(repo_name, Optional) else repo.repo_name,
870 871
871 872 fork_id=fork_of
872 873 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
873 874
874 875 user=owner
875 876 if not isinstance(owner, Optional) else repo.user.username,
876 877
877 878 repo_description=description
878 879 if not isinstance(description, Optional) else repo.description,
879 880
880 881 repo_private=private
881 882 if not isinstance(private, Optional) else repo.private,
882 883
883 884 clone_uri=clone_uri
884 885 if not isinstance(clone_uri, Optional) else repo.clone_uri,
885 886
886 887 push_uri=push_uri
887 888 if not isinstance(push_uri, Optional) else repo.push_uri,
888 889
889 890 repo_landing_rev=landing_rev
890 891 if not isinstance(landing_rev, Optional) else repo._landing_revision,
891 892
892 893 repo_enable_statistics=enable_statistics
893 894 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
894 895
895 896 repo_enable_locking=enable_locking
896 897 if not isinstance(enable_locking, Optional) else repo.enable_locking,
897 898
898 899 repo_enable_downloads=enable_downloads
899 900 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
900 901
901 902 ref_choices, _labels = ScmModel().get_repo_landing_revs(
902 903 request.translate, repo=repo)
903 904
904 905 old_values = repo.get_api_data()
906 repo_type = repo.repo_type
905 907 schema = repo_schema.RepoSchema().bind(
906 908 repo_type_options=rhodecode.BACKENDS.keys(),
907 909 repo_ref_options=ref_choices,
910 repo_type=repo_type,
908 911 # user caller
909 912 user=apiuser,
910 913 old_values=old_values)
911 914 try:
912 915 schema_data = schema.deserialize(dict(
913 916 # we save old value, users cannot change type
914 repo_type=repo.repo_type,
917 repo_type=repo_type,
915 918
916 919 repo_name=updates['repo_name'],
917 920 repo_owner=updates['user'],
918 921 repo_description=updates['repo_description'],
919 922 repo_clone_uri=updates['clone_uri'],
920 923 repo_push_uri=updates['push_uri'],
921 924 repo_fork_of=updates['fork_id'],
922 925 repo_private=updates['repo_private'],
923 926 repo_landing_commit_ref=updates['repo_landing_rev'],
924 927 repo_enable_statistics=updates['repo_enable_statistics'],
925 928 repo_enable_downloads=updates['repo_enable_downloads'],
926 929 repo_enable_locking=updates['repo_enable_locking']))
927 930 except validation_schema.Invalid as err:
928 931 raise JSONRPCValidationError(colander_exc=err)
929 932
930 933 # save validated data back into the updates dict
931 934 validated_updates = dict(
932 935 repo_name=schema_data['repo_group']['repo_name_without_group'],
933 936 repo_group=schema_data['repo_group']['repo_group_id'],
934 937
935 938 user=schema_data['repo_owner'],
936 939 repo_description=schema_data['repo_description'],
937 940 repo_private=schema_data['repo_private'],
938 941 clone_uri=schema_data['repo_clone_uri'],
939 942 push_uri=schema_data['repo_push_uri'],
940 943 repo_landing_rev=schema_data['repo_landing_commit_ref'],
941 944 repo_enable_statistics=schema_data['repo_enable_statistics'],
942 945 repo_enable_locking=schema_data['repo_enable_locking'],
943 946 repo_enable_downloads=schema_data['repo_enable_downloads'],
944 947 )
945 948
946 949 if schema_data['repo_fork_of']:
947 950 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
948 951 validated_updates['fork_id'] = fork_repo.repo_id
949 952
950 953 # extra fields
951 954 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
952 955 if fields:
953 956 validated_updates.update(fields)
954 957
955 958 try:
956 959 RepoModel().update(repo, **validated_updates)
957 960 audit_logger.store_api(
958 961 'repo.edit', action_data={'old_data': old_values},
959 962 user=apiuser, repo=repo)
960 963 Session().commit()
961 964 return {
962 965 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
963 966 'repository': repo.get_api_data(include_secrets=include_secrets)
964 967 }
965 968 except Exception:
966 969 log.exception(
967 970 u"Exception while trying to update the repository %s",
968 971 repoid)
969 972 raise JSONRPCError('failed to update repo `%s`' % repoid)
970 973
971 974
972 975 @jsonrpc_method()
973 976 def fork_repo(request, apiuser, repoid, fork_name,
974 977 owner=Optional(OAttr('apiuser')),
975 978 description=Optional(''),
976 979 private=Optional(False),
977 980 clone_uri=Optional(None),
978 981 landing_rev=Optional('rev:tip'),
979 982 copy_permissions=Optional(False)):
980 983 """
981 984 Creates a fork of the specified |repo|.
982 985
983 986 * If the fork_name contains "/", fork will be created inside
984 987 a repository group or nested repository groups
985 988
986 989 For example "foo/bar/fork-repo" will create fork called "fork-repo"
987 990 inside group "foo/bar". You have to have permissions to access and
988 991 write to the last repository group ("bar" in this example)
989 992
990 993 This command can only be run using an |authtoken| with minimum
991 994 read permissions of the forked repo, create fork permissions for an user.
992 995
993 996 :param apiuser: This is filled automatically from the |authtoken|.
994 997 :type apiuser: AuthUser
995 998 :param repoid: Set repository name or repository ID.
996 999 :type repoid: str or int
997 1000 :param fork_name: Set the fork name, including it's repository group membership.
998 1001 :type fork_name: str
999 1002 :param owner: Set the fork owner.
1000 1003 :type owner: str
1001 1004 :param description: Set the fork description.
1002 1005 :type description: str
1003 1006 :param copy_permissions: Copy permissions from parent |repo|. The
1004 1007 default is False.
1005 1008 :type copy_permissions: bool
1006 1009 :param private: Make the fork private. The default is False.
1007 1010 :type private: bool
1008 1011 :param landing_rev: Set the landing revision. The default is tip.
1009 1012
1010 1013 Example output:
1011 1014
1012 1015 .. code-block:: bash
1013 1016
1014 1017 id : <id_for_response>
1015 1018 api_key : "<api_key>"
1016 1019 args: {
1017 1020 "repoid" : "<reponame or repo_id>",
1018 1021 "fork_name": "<forkname>",
1019 1022 "owner": "<username or user_id = Optional(=apiuser)>",
1020 1023 "description": "<description>",
1021 1024 "copy_permissions": "<bool>",
1022 1025 "private": "<bool>",
1023 1026 "landing_rev": "<landing_rev>"
1024 1027 }
1025 1028
1026 1029 Example error output:
1027 1030
1028 1031 .. code-block:: bash
1029 1032
1030 1033 id : <id_given_in_input>
1031 1034 result: {
1032 1035 "msg": "Created fork of `<reponame>` as `<forkname>`",
1033 1036 "success": true,
1034 1037 "task": "<celery task id or None if done sync>"
1035 1038 }
1036 1039 error: null
1037 1040
1038 1041 """
1039 1042
1040 1043 repo = get_repo_or_error(repoid)
1041 1044 repo_name = repo.repo_name
1042 1045
1043 1046 if not has_superadmin_permission(apiuser):
1044 1047 # check if we have at least read permission for
1045 1048 # this repo that we fork !
1046 1049 _perms = (
1047 1050 'repository.admin', 'repository.write', 'repository.read')
1048 1051 validate_repo_permissions(apiuser, repoid, repo, _perms)
1049 1052
1050 1053 # check if the regular user has at least fork permissions as well
1051 1054 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1052 1055 raise JSONRPCForbidden()
1053 1056
1054 1057 # check if user can set owner parameter
1055 1058 owner = validate_set_owner_permissions(apiuser, owner)
1056 1059
1057 1060 description = Optional.extract(description)
1058 1061 copy_permissions = Optional.extract(copy_permissions)
1059 1062 clone_uri = Optional.extract(clone_uri)
1060 1063 landing_commit_ref = Optional.extract(landing_rev)
1061 1064 private = Optional.extract(private)
1062 1065
1063 1066 schema = repo_schema.RepoSchema().bind(
1064 1067 repo_type_options=rhodecode.BACKENDS.keys(),
1068 repo_type=repo.repo_type,
1065 1069 # user caller
1066 1070 user=apiuser)
1067 1071
1068 1072 try:
1069 1073 schema_data = schema.deserialize(dict(
1070 1074 repo_name=fork_name,
1071 1075 repo_type=repo.repo_type,
1072 1076 repo_owner=owner.username,
1073 1077 repo_description=description,
1074 1078 repo_landing_commit_ref=landing_commit_ref,
1075 1079 repo_clone_uri=clone_uri,
1076 1080 repo_private=private,
1077 1081 repo_copy_permissions=copy_permissions))
1078 1082 except validation_schema.Invalid as err:
1079 1083 raise JSONRPCValidationError(colander_exc=err)
1080 1084
1081 1085 try:
1082 1086 data = {
1083 1087 'fork_parent_id': repo.repo_id,
1084 1088
1085 1089 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1086 1090 'repo_name_full': schema_data['repo_name'],
1087 1091 'repo_group': schema_data['repo_group']['repo_group_id'],
1088 1092 'repo_type': schema_data['repo_type'],
1089 1093 'description': schema_data['repo_description'],
1090 1094 'private': schema_data['repo_private'],
1091 1095 'copy_permissions': schema_data['repo_copy_permissions'],
1092 1096 'landing_rev': schema_data['repo_landing_commit_ref'],
1093 1097 }
1094 1098
1095 1099 task = RepoModel().create_fork(data, cur_user=owner)
1096 1100 # no commit, it's done in RepoModel, or async via celery
1097 1101 task_id = get_task_id(task)
1098 1102
1099 1103 return {
1100 1104 'msg': 'Created fork of `%s` as `%s`' % (
1101 1105 repo.repo_name, schema_data['repo_name']),
1102 1106 'success': True, # cannot return the repo data here since fork
1103 1107 # can be done async
1104 1108 'task': task_id
1105 1109 }
1106 1110 except Exception:
1107 1111 log.exception(
1108 1112 u"Exception while trying to create fork %s",
1109 1113 schema_data['repo_name'])
1110 1114 raise JSONRPCError(
1111 1115 'failed to fork repository `%s` as `%s`' % (
1112 1116 repo_name, schema_data['repo_name']))
1113 1117
1114 1118
1115 1119 @jsonrpc_method()
1116 1120 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1117 1121 """
1118 1122 Deletes a repository.
1119 1123
1120 1124 * When the `forks` parameter is set it's possible to detach or delete
1121 1125 forks of deleted repository.
1122 1126
1123 1127 This command can only be run using an |authtoken| with admin
1124 1128 permissions on the |repo|.
1125 1129
1126 1130 :param apiuser: This is filled automatically from the |authtoken|.
1127 1131 :type apiuser: AuthUser
1128 1132 :param repoid: Set the repository name or repository ID.
1129 1133 :type repoid: str or int
1130 1134 :param forks: Set to `detach` or `delete` forks from the |repo|.
1131 1135 :type forks: Optional(str)
1132 1136
1133 1137 Example error output:
1134 1138
1135 1139 .. code-block:: bash
1136 1140
1137 1141 id : <id_given_in_input>
1138 1142 result: {
1139 1143 "msg": "Deleted repository `<reponame>`",
1140 1144 "success": true
1141 1145 }
1142 1146 error: null
1143 1147 """
1144 1148
1145 1149 repo = get_repo_or_error(repoid)
1146 1150 repo_name = repo.repo_name
1147 1151 if not has_superadmin_permission(apiuser):
1148 1152 _perms = ('repository.admin',)
1149 1153 validate_repo_permissions(apiuser, repoid, repo, _perms)
1150 1154
1151 1155 try:
1152 1156 handle_forks = Optional.extract(forks)
1153 1157 _forks_msg = ''
1154 1158 _forks = [f for f in repo.forks]
1155 1159 if handle_forks == 'detach':
1156 1160 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1157 1161 elif handle_forks == 'delete':
1158 1162 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1159 1163 elif _forks:
1160 1164 raise JSONRPCError(
1161 1165 'Cannot delete `%s` it still contains attached forks' %
1162 1166 (repo.repo_name,)
1163 1167 )
1164 1168 old_data = repo.get_api_data()
1165 1169 RepoModel().delete(repo, forks=forks)
1166 1170
1167 1171 repo = audit_logger.RepoWrap(repo_id=None,
1168 1172 repo_name=repo.repo_name)
1169 1173
1170 1174 audit_logger.store_api(
1171 1175 'repo.delete', action_data={'old_data': old_data},
1172 1176 user=apiuser, repo=repo)
1173 1177
1174 1178 ScmModel().mark_for_invalidation(repo_name, delete=True)
1175 1179 Session().commit()
1176 1180 return {
1177 1181 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
1178 1182 'success': True
1179 1183 }
1180 1184 except Exception:
1181 1185 log.exception("Exception occurred while trying to delete repo")
1182 1186 raise JSONRPCError(
1183 1187 'failed to delete repository `%s`' % (repo_name,)
1184 1188 )
1185 1189
1186 1190
1187 1191 #TODO: marcink, change name ?
1188 1192 @jsonrpc_method()
1189 1193 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1190 1194 """
1191 1195 Invalidates the cache for the specified repository.
1192 1196
1193 1197 This command can only be run using an |authtoken| with admin rights to
1194 1198 the specified repository.
1195 1199
1196 1200 This command takes the following options:
1197 1201
1198 1202 :param apiuser: This is filled automatically from |authtoken|.
1199 1203 :type apiuser: AuthUser
1200 1204 :param repoid: Sets the repository name or repository ID.
1201 1205 :type repoid: str or int
1202 1206 :param delete_keys: This deletes the invalidated keys instead of
1203 1207 just flagging them.
1204 1208 :type delete_keys: Optional(``True`` | ``False``)
1205 1209
1206 1210 Example output:
1207 1211
1208 1212 .. code-block:: bash
1209 1213
1210 1214 id : <id_given_in_input>
1211 1215 result : {
1212 1216 'msg': Cache for repository `<repository name>` was invalidated,
1213 1217 'repository': <repository name>
1214 1218 }
1215 1219 error : null
1216 1220
1217 1221 Example error output:
1218 1222
1219 1223 .. code-block:: bash
1220 1224
1221 1225 id : <id_given_in_input>
1222 1226 result : null
1223 1227 error : {
1224 1228 'Error occurred during cache invalidation action'
1225 1229 }
1226 1230
1227 1231 """
1228 1232
1229 1233 repo = get_repo_or_error(repoid)
1230 1234 if not has_superadmin_permission(apiuser):
1231 1235 _perms = ('repository.admin', 'repository.write',)
1232 1236 validate_repo_permissions(apiuser, repoid, repo, _perms)
1233 1237
1234 1238 delete = Optional.extract(delete_keys)
1235 1239 try:
1236 1240 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1237 1241 return {
1238 1242 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1239 1243 'repository': repo.repo_name
1240 1244 }
1241 1245 except Exception:
1242 1246 log.exception(
1243 1247 "Exception occurred while trying to invalidate repo cache")
1244 1248 raise JSONRPCError(
1245 1249 'Error occurred during cache invalidation action'
1246 1250 )
1247 1251
1248 1252
1249 1253 #TODO: marcink, change name ?
1250 1254 @jsonrpc_method()
1251 1255 def lock(request, apiuser, repoid, locked=Optional(None),
1252 1256 userid=Optional(OAttr('apiuser'))):
1253 1257 """
1254 1258 Sets the lock state of the specified |repo| by the given user.
1255 1259 From more information, see :ref:`repo-locking`.
1256 1260
1257 1261 * If the ``userid`` option is not set, the repository is locked to the
1258 1262 user who called the method.
1259 1263 * If the ``locked`` parameter is not set, the current lock state of the
1260 1264 repository is displayed.
1261 1265
1262 1266 This command can only be run using an |authtoken| with admin rights to
1263 1267 the specified repository.
1264 1268
1265 1269 This command takes the following options:
1266 1270
1267 1271 :param apiuser: This is filled automatically from the |authtoken|.
1268 1272 :type apiuser: AuthUser
1269 1273 :param repoid: Sets the repository name or repository ID.
1270 1274 :type repoid: str or int
1271 1275 :param locked: Sets the lock state.
1272 1276 :type locked: Optional(``True`` | ``False``)
1273 1277 :param userid: Set the repository lock to this user.
1274 1278 :type userid: Optional(str or int)
1275 1279
1276 1280 Example error output:
1277 1281
1278 1282 .. code-block:: bash
1279 1283
1280 1284 id : <id_given_in_input>
1281 1285 result : {
1282 1286 'repo': '<reponame>',
1283 1287 'locked': <bool: lock state>,
1284 1288 'locked_since': <int: lock timestamp>,
1285 1289 'locked_by': <username of person who made the lock>,
1286 1290 'lock_reason': <str: reason for locking>,
1287 1291 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1288 1292 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1289 1293 or
1290 1294 'msg': 'Repo `<repository name>` not locked.'
1291 1295 or
1292 1296 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1293 1297 }
1294 1298 error : null
1295 1299
1296 1300 Example error output:
1297 1301
1298 1302 .. code-block:: bash
1299 1303
1300 1304 id : <id_given_in_input>
1301 1305 result : null
1302 1306 error : {
1303 1307 'Error occurred locking repository `<reponame>`'
1304 1308 }
1305 1309 """
1306 1310
1307 1311 repo = get_repo_or_error(repoid)
1308 1312 if not has_superadmin_permission(apiuser):
1309 1313 # check if we have at least write permission for this repo !
1310 1314 _perms = ('repository.admin', 'repository.write',)
1311 1315 validate_repo_permissions(apiuser, repoid, repo, _perms)
1312 1316
1313 1317 # make sure normal user does not pass someone else userid,
1314 1318 # he is not allowed to do that
1315 1319 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1316 1320 raise JSONRPCError('userid is not the same as your user')
1317 1321
1318 1322 if isinstance(userid, Optional):
1319 1323 userid = apiuser.user_id
1320 1324
1321 1325 user = get_user_or_error(userid)
1322 1326
1323 1327 if isinstance(locked, Optional):
1324 1328 lockobj = repo.locked
1325 1329
1326 1330 if lockobj[0] is None:
1327 1331 _d = {
1328 1332 'repo': repo.repo_name,
1329 1333 'locked': False,
1330 1334 'locked_since': None,
1331 1335 'locked_by': None,
1332 1336 'lock_reason': None,
1333 1337 'lock_state_changed': False,
1334 1338 'msg': 'Repo `%s` not locked.' % repo.repo_name
1335 1339 }
1336 1340 return _d
1337 1341 else:
1338 1342 _user_id, _time, _reason = lockobj
1339 1343 lock_user = get_user_or_error(userid)
1340 1344 _d = {
1341 1345 'repo': repo.repo_name,
1342 1346 'locked': True,
1343 1347 'locked_since': _time,
1344 1348 'locked_by': lock_user.username,
1345 1349 'lock_reason': _reason,
1346 1350 'lock_state_changed': False,
1347 1351 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1348 1352 % (repo.repo_name, lock_user.username,
1349 1353 json.dumps(time_to_datetime(_time))))
1350 1354 }
1351 1355 return _d
1352 1356
1353 1357 # force locked state through a flag
1354 1358 else:
1355 1359 locked = str2bool(locked)
1356 1360 lock_reason = Repository.LOCK_API
1357 1361 try:
1358 1362 if locked:
1359 1363 lock_time = time.time()
1360 1364 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1361 1365 else:
1362 1366 lock_time = None
1363 1367 Repository.unlock(repo)
1364 1368 _d = {
1365 1369 'repo': repo.repo_name,
1366 1370 'locked': locked,
1367 1371 'locked_since': lock_time,
1368 1372 'locked_by': user.username,
1369 1373 'lock_reason': lock_reason,
1370 1374 'lock_state_changed': True,
1371 1375 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1372 1376 % (user.username, repo.repo_name, locked))
1373 1377 }
1374 1378 return _d
1375 1379 except Exception:
1376 1380 log.exception(
1377 1381 "Exception occurred while trying to lock repository")
1378 1382 raise JSONRPCError(
1379 1383 'Error occurred locking repository `%s`' % repo.repo_name
1380 1384 )
1381 1385
1382 1386
1383 1387 @jsonrpc_method()
1384 1388 def comment_commit(
1385 1389 request, apiuser, repoid, commit_id, message, status=Optional(None),
1386 1390 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1387 1391 resolves_comment_id=Optional(None),
1388 1392 userid=Optional(OAttr('apiuser'))):
1389 1393 """
1390 1394 Set a commit comment, and optionally change the status of the commit.
1391 1395
1392 1396 :param apiuser: This is filled automatically from the |authtoken|.
1393 1397 :type apiuser: AuthUser
1394 1398 :param repoid: Set the repository name or repository ID.
1395 1399 :type repoid: str or int
1396 1400 :param commit_id: Specify the commit_id for which to set a comment.
1397 1401 :type commit_id: str
1398 1402 :param message: The comment text.
1399 1403 :type message: str
1400 1404 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1401 1405 'approved', 'rejected', 'under_review'
1402 1406 :type status: str
1403 1407 :param comment_type: Comment type, one of: 'note', 'todo'
1404 1408 :type comment_type: Optional(str), default: 'note'
1405 1409 :param userid: Set the user name of the comment creator.
1406 1410 :type userid: Optional(str or int)
1407 1411
1408 1412 Example error output:
1409 1413
1410 1414 .. code-block:: bash
1411 1415
1412 1416 {
1413 1417 "id" : <id_given_in_input>,
1414 1418 "result" : {
1415 1419 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1416 1420 "status_change": null or <status>,
1417 1421 "success": true
1418 1422 },
1419 1423 "error" : null
1420 1424 }
1421 1425
1422 1426 """
1423 1427 repo = get_repo_or_error(repoid)
1424 1428 if not has_superadmin_permission(apiuser):
1425 1429 _perms = ('repository.read', 'repository.write', 'repository.admin')
1426 1430 validate_repo_permissions(apiuser, repoid, repo, _perms)
1427 1431
1428 1432 try:
1429 1433 commit_id = repo.scm_instance().get_commit(commit_id=commit_id).raw_id
1430 1434 except Exception as e:
1431 1435 log.exception('Failed to fetch commit')
1432 1436 raise JSONRPCError(e.message)
1433 1437
1434 1438 if isinstance(userid, Optional):
1435 1439 userid = apiuser.user_id
1436 1440
1437 1441 user = get_user_or_error(userid)
1438 1442 status = Optional.extract(status)
1439 1443 comment_type = Optional.extract(comment_type)
1440 1444 resolves_comment_id = Optional.extract(resolves_comment_id)
1441 1445
1442 1446 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1443 1447 if status and status not in allowed_statuses:
1444 1448 raise JSONRPCError('Bad status, must be on '
1445 1449 'of %s got %s' % (allowed_statuses, status,))
1446 1450
1447 1451 if resolves_comment_id:
1448 1452 comment = ChangesetComment.get(resolves_comment_id)
1449 1453 if not comment:
1450 1454 raise JSONRPCError(
1451 1455 'Invalid resolves_comment_id `%s` for this commit.'
1452 1456 % resolves_comment_id)
1453 1457 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1454 1458 raise JSONRPCError(
1455 1459 'Comment `%s` is wrong type for setting status to resolved.'
1456 1460 % resolves_comment_id)
1457 1461
1458 1462 try:
1459 1463 rc_config = SettingsModel().get_all_settings()
1460 1464 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1461 1465 status_change_label = ChangesetStatus.get_status_lbl(status)
1462 1466 comment = CommentsModel().create(
1463 1467 message, repo, user, commit_id=commit_id,
1464 1468 status_change=status_change_label,
1465 1469 status_change_type=status,
1466 1470 renderer=renderer,
1467 1471 comment_type=comment_type,
1468 1472 resolves_comment_id=resolves_comment_id
1469 1473 )
1470 1474 if status:
1471 1475 # also do a status change
1472 1476 try:
1473 1477 ChangesetStatusModel().set_status(
1474 1478 repo, status, user, comment, revision=commit_id,
1475 1479 dont_allow_on_closed_pull_request=True
1476 1480 )
1477 1481 except StatusChangeOnClosedPullRequestError:
1478 1482 log.exception(
1479 1483 "Exception occurred while trying to change repo commit status")
1480 1484 msg = ('Changing status on a changeset associated with '
1481 1485 'a closed pull request is not allowed')
1482 1486 raise JSONRPCError(msg)
1483 1487
1484 1488 Session().commit()
1485 1489 return {
1486 1490 'msg': (
1487 1491 'Commented on commit `%s` for repository `%s`' % (
1488 1492 comment.revision, repo.repo_name)),
1489 1493 'status_change': status,
1490 1494 'success': True,
1491 1495 }
1492 1496 except JSONRPCError:
1493 1497 # catch any inside errors, and re-raise them to prevent from
1494 1498 # below global catch to silence them
1495 1499 raise
1496 1500 except Exception:
1497 1501 log.exception("Exception occurred while trying to comment on commit")
1498 1502 raise JSONRPCError(
1499 1503 'failed to set comment on repository `%s`' % (repo.repo_name,)
1500 1504 )
1501 1505
1502 1506
1503 1507 @jsonrpc_method()
1504 1508 def grant_user_permission(request, apiuser, repoid, userid, perm):
1505 1509 """
1506 1510 Grant permissions for the specified user on the given repository,
1507 1511 or update existing permissions if found.
1508 1512
1509 1513 This command can only be run using an |authtoken| with admin
1510 1514 permissions on the |repo|.
1511 1515
1512 1516 :param apiuser: This is filled automatically from the |authtoken|.
1513 1517 :type apiuser: AuthUser
1514 1518 :param repoid: Set the repository name or repository ID.
1515 1519 :type repoid: str or int
1516 1520 :param userid: Set the user name.
1517 1521 :type userid: str
1518 1522 :param perm: Set the user permissions, using the following format
1519 1523 ``(repository.(none|read|write|admin))``
1520 1524 :type perm: str
1521 1525
1522 1526 Example output:
1523 1527
1524 1528 .. code-block:: bash
1525 1529
1526 1530 id : <id_given_in_input>
1527 1531 result: {
1528 1532 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1529 1533 "success": true
1530 1534 }
1531 1535 error: null
1532 1536 """
1533 1537
1534 1538 repo = get_repo_or_error(repoid)
1535 1539 user = get_user_or_error(userid)
1536 1540 perm = get_perm_or_error(perm)
1537 1541 if not has_superadmin_permission(apiuser):
1538 1542 _perms = ('repository.admin',)
1539 1543 validate_repo_permissions(apiuser, repoid, repo, _perms)
1540 1544
1541 1545 try:
1542 1546
1543 1547 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
1544 1548
1545 1549 Session().commit()
1546 1550 return {
1547 1551 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1548 1552 perm.permission_name, user.username, repo.repo_name
1549 1553 ),
1550 1554 'success': True
1551 1555 }
1552 1556 except Exception:
1553 1557 log.exception(
1554 1558 "Exception occurred while trying edit permissions for repo")
1555 1559 raise JSONRPCError(
1556 1560 'failed to edit permission for user: `%s` in repo: `%s`' % (
1557 1561 userid, repoid
1558 1562 )
1559 1563 )
1560 1564
1561 1565
1562 1566 @jsonrpc_method()
1563 1567 def revoke_user_permission(request, apiuser, repoid, userid):
1564 1568 """
1565 1569 Revoke permission for a user on the specified repository.
1566 1570
1567 1571 This command can only be run using an |authtoken| with admin
1568 1572 permissions on the |repo|.
1569 1573
1570 1574 :param apiuser: This is filled automatically from the |authtoken|.
1571 1575 :type apiuser: AuthUser
1572 1576 :param repoid: Set the repository name or repository ID.
1573 1577 :type repoid: str or int
1574 1578 :param userid: Set the user name of revoked user.
1575 1579 :type userid: str or int
1576 1580
1577 1581 Example error output:
1578 1582
1579 1583 .. code-block:: bash
1580 1584
1581 1585 id : <id_given_in_input>
1582 1586 result: {
1583 1587 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1584 1588 "success": true
1585 1589 }
1586 1590 error: null
1587 1591 """
1588 1592
1589 1593 repo = get_repo_or_error(repoid)
1590 1594 user = get_user_or_error(userid)
1591 1595 if not has_superadmin_permission(apiuser):
1592 1596 _perms = ('repository.admin',)
1593 1597 validate_repo_permissions(apiuser, repoid, repo, _perms)
1594 1598
1595 1599 try:
1596 1600 RepoModel().revoke_user_permission(repo=repo, user=user)
1597 1601 Session().commit()
1598 1602 return {
1599 1603 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1600 1604 user.username, repo.repo_name
1601 1605 ),
1602 1606 'success': True
1603 1607 }
1604 1608 except Exception:
1605 1609 log.exception(
1606 1610 "Exception occurred while trying revoke permissions to repo")
1607 1611 raise JSONRPCError(
1608 1612 'failed to edit permission for user: `%s` in repo: `%s`' % (
1609 1613 userid, repoid
1610 1614 )
1611 1615 )
1612 1616
1613 1617
1614 1618 @jsonrpc_method()
1615 1619 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1616 1620 """
1617 1621 Grant permission for a user group on the specified repository,
1618 1622 or update existing permissions.
1619 1623
1620 1624 This command can only be run using an |authtoken| with admin
1621 1625 permissions on the |repo|.
1622 1626
1623 1627 :param apiuser: This is filled automatically from the |authtoken|.
1624 1628 :type apiuser: AuthUser
1625 1629 :param repoid: Set the repository name or repository ID.
1626 1630 :type repoid: str or int
1627 1631 :param usergroupid: Specify the ID of the user group.
1628 1632 :type usergroupid: str or int
1629 1633 :param perm: Set the user group permissions using the following
1630 1634 format: (repository.(none|read|write|admin))
1631 1635 :type perm: str
1632 1636
1633 1637 Example output:
1634 1638
1635 1639 .. code-block:: bash
1636 1640
1637 1641 id : <id_given_in_input>
1638 1642 result : {
1639 1643 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1640 1644 "success": true
1641 1645
1642 1646 }
1643 1647 error : null
1644 1648
1645 1649 Example error output:
1646 1650
1647 1651 .. code-block:: bash
1648 1652
1649 1653 id : <id_given_in_input>
1650 1654 result : null
1651 1655 error : {
1652 1656 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1653 1657 }
1654 1658
1655 1659 """
1656 1660
1657 1661 repo = get_repo_or_error(repoid)
1658 1662 perm = get_perm_or_error(perm)
1659 1663 if not has_superadmin_permission(apiuser):
1660 1664 _perms = ('repository.admin',)
1661 1665 validate_repo_permissions(apiuser, repoid, repo, _perms)
1662 1666
1663 1667 user_group = get_user_group_or_error(usergroupid)
1664 1668 if not has_superadmin_permission(apiuser):
1665 1669 # check if we have at least read permission for this user group !
1666 1670 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1667 1671 if not HasUserGroupPermissionAnyApi(*_perms)(
1668 1672 user=apiuser, user_group_name=user_group.users_group_name):
1669 1673 raise JSONRPCError(
1670 1674 'user group `%s` does not exist' % (usergroupid,))
1671 1675
1672 1676 try:
1673 1677 RepoModel().grant_user_group_permission(
1674 1678 repo=repo, group_name=user_group, perm=perm)
1675 1679
1676 1680 Session().commit()
1677 1681 return {
1678 1682 'msg': 'Granted perm: `%s` for user group: `%s` in '
1679 1683 'repo: `%s`' % (
1680 1684 perm.permission_name, user_group.users_group_name,
1681 1685 repo.repo_name
1682 1686 ),
1683 1687 'success': True
1684 1688 }
1685 1689 except Exception:
1686 1690 log.exception(
1687 1691 "Exception occurred while trying change permission on repo")
1688 1692 raise JSONRPCError(
1689 1693 'failed to edit permission for user group: `%s` in '
1690 1694 'repo: `%s`' % (
1691 1695 usergroupid, repo.repo_name
1692 1696 )
1693 1697 )
1694 1698
1695 1699
1696 1700 @jsonrpc_method()
1697 1701 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1698 1702 """
1699 1703 Revoke the permissions of a user group on a given repository.
1700 1704
1701 1705 This command can only be run using an |authtoken| with admin
1702 1706 permissions on the |repo|.
1703 1707
1704 1708 :param apiuser: This is filled automatically from the |authtoken|.
1705 1709 :type apiuser: AuthUser
1706 1710 :param repoid: Set the repository name or repository ID.
1707 1711 :type repoid: str or int
1708 1712 :param usergroupid: Specify the user group ID.
1709 1713 :type usergroupid: str or int
1710 1714
1711 1715 Example output:
1712 1716
1713 1717 .. code-block:: bash
1714 1718
1715 1719 id : <id_given_in_input>
1716 1720 result: {
1717 1721 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1718 1722 "success": true
1719 1723 }
1720 1724 error: null
1721 1725 """
1722 1726
1723 1727 repo = get_repo_or_error(repoid)
1724 1728 if not has_superadmin_permission(apiuser):
1725 1729 _perms = ('repository.admin',)
1726 1730 validate_repo_permissions(apiuser, repoid, repo, _perms)
1727 1731
1728 1732 user_group = get_user_group_or_error(usergroupid)
1729 1733 if not has_superadmin_permission(apiuser):
1730 1734 # check if we have at least read permission for this user group !
1731 1735 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1732 1736 if not HasUserGroupPermissionAnyApi(*_perms)(
1733 1737 user=apiuser, user_group_name=user_group.users_group_name):
1734 1738 raise JSONRPCError(
1735 1739 'user group `%s` does not exist' % (usergroupid,))
1736 1740
1737 1741 try:
1738 1742 RepoModel().revoke_user_group_permission(
1739 1743 repo=repo, group_name=user_group)
1740 1744
1741 1745 Session().commit()
1742 1746 return {
1743 1747 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1744 1748 user_group.users_group_name, repo.repo_name
1745 1749 ),
1746 1750 'success': True
1747 1751 }
1748 1752 except Exception:
1749 1753 log.exception("Exception occurred while trying revoke "
1750 1754 "user group permission on repo")
1751 1755 raise JSONRPCError(
1752 1756 'failed to edit permission for user group: `%s` in '
1753 1757 'repo: `%s`' % (
1754 1758 user_group.users_group_name, repo.repo_name
1755 1759 )
1756 1760 )
1757 1761
1758 1762
1759 1763 @jsonrpc_method()
1760 1764 def pull(request, apiuser, repoid, remote_uri=Optional(None)):
1761 1765 """
1762 1766 Triggers a pull on the given repository from a remote location. You
1763 1767 can use this to keep remote repositories up-to-date.
1764 1768
1765 1769 This command can only be run using an |authtoken| with admin
1766 1770 rights to the specified repository. For more information,
1767 1771 see :ref:`config-token-ref`.
1768 1772
1769 1773 This command takes the following options:
1770 1774
1771 1775 :param apiuser: This is filled automatically from the |authtoken|.
1772 1776 :type apiuser: AuthUser
1773 1777 :param repoid: The repository name or repository ID.
1774 1778 :type repoid: str or int
1775 1779 :param remote_uri: Optional remote URI to pass in for pull
1776 1780 :type remote_uri: str
1777 1781
1778 1782 Example output:
1779 1783
1780 1784 .. code-block:: bash
1781 1785
1782 1786 id : <id_given_in_input>
1783 1787 result : {
1784 1788 "msg": "Pulled from url `<remote_url>` on repo `<repository name>`"
1785 1789 "repository": "<repository name>"
1786 1790 }
1787 1791 error : null
1788 1792
1789 1793 Example error output:
1790 1794
1791 1795 .. code-block:: bash
1792 1796
1793 1797 id : <id_given_in_input>
1794 1798 result : null
1795 1799 error : {
1796 1800 "Unable to push changes from `<remote_url>`"
1797 1801 }
1798 1802
1799 1803 """
1800 1804
1801 1805 repo = get_repo_or_error(repoid)
1802 1806 remote_uri = Optional.extract(remote_uri)
1803 1807 remote_uri_display = remote_uri or repo.clone_uri_hidden
1804 1808 if not has_superadmin_permission(apiuser):
1805 1809 _perms = ('repository.admin',)
1806 1810 validate_repo_permissions(apiuser, repoid, repo, _perms)
1807 1811
1808 1812 try:
1809 1813 ScmModel().pull_changes(
1810 1814 repo.repo_name, apiuser.username, remote_uri=remote_uri)
1811 1815 return {
1812 1816 'msg': 'Pulled from url `%s` on repo `%s`' % (
1813 1817 remote_uri_display, repo.repo_name),
1814 1818 'repository': repo.repo_name
1815 1819 }
1816 1820 except Exception:
1817 1821 log.exception("Exception occurred while trying to "
1818 1822 "pull changes from remote location")
1819 1823 raise JSONRPCError(
1820 1824 'Unable to pull changes from `%s`' % remote_uri_display
1821 1825 )
1822 1826
1823 1827
1824 1828 @jsonrpc_method()
1825 1829 def strip(request, apiuser, repoid, revision, branch):
1826 1830 """
1827 1831 Strips the given revision from the specified repository.
1828 1832
1829 1833 * This will remove the revision and all of its decendants.
1830 1834
1831 1835 This command can only be run using an |authtoken| with admin rights to
1832 1836 the specified repository.
1833 1837
1834 1838 This command takes the following options:
1835 1839
1836 1840 :param apiuser: This is filled automatically from the |authtoken|.
1837 1841 :type apiuser: AuthUser
1838 1842 :param repoid: The repository name or repository ID.
1839 1843 :type repoid: str or int
1840 1844 :param revision: The revision you wish to strip.
1841 1845 :type revision: str
1842 1846 :param branch: The branch from which to strip the revision.
1843 1847 :type branch: str
1844 1848
1845 1849 Example output:
1846 1850
1847 1851 .. code-block:: bash
1848 1852
1849 1853 id : <id_given_in_input>
1850 1854 result : {
1851 1855 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
1852 1856 "repository": "<repository name>"
1853 1857 }
1854 1858 error : null
1855 1859
1856 1860 Example error output:
1857 1861
1858 1862 .. code-block:: bash
1859 1863
1860 1864 id : <id_given_in_input>
1861 1865 result : null
1862 1866 error : {
1863 1867 "Unable to strip commit <commit_hash> from repo `<repository name>`"
1864 1868 }
1865 1869
1866 1870 """
1867 1871
1868 1872 repo = get_repo_or_error(repoid)
1869 1873 if not has_superadmin_permission(apiuser):
1870 1874 _perms = ('repository.admin',)
1871 1875 validate_repo_permissions(apiuser, repoid, repo, _perms)
1872 1876
1873 1877 try:
1874 1878 ScmModel().strip(repo, revision, branch)
1875 1879 audit_logger.store_api(
1876 1880 'repo.commit.strip', action_data={'commit_id': revision},
1877 1881 repo=repo,
1878 1882 user=apiuser, commit=True)
1879 1883
1880 1884 return {
1881 1885 'msg': 'Stripped commit %s from repo `%s`' % (
1882 1886 revision, repo.repo_name),
1883 1887 'repository': repo.repo_name
1884 1888 }
1885 1889 except Exception:
1886 1890 log.exception("Exception while trying to strip")
1887 1891 raise JSONRPCError(
1888 1892 'Unable to strip commit %s from repo `%s`' % (
1889 1893 revision, repo.repo_name)
1890 1894 )
1891 1895
1892 1896
1893 1897 @jsonrpc_method()
1894 1898 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
1895 1899 """
1896 1900 Returns all settings for a repository. If key is given it only returns the
1897 1901 setting identified by the key or null.
1898 1902
1899 1903 :param apiuser: This is filled automatically from the |authtoken|.
1900 1904 :type apiuser: AuthUser
1901 1905 :param repoid: The repository name or repository id.
1902 1906 :type repoid: str or int
1903 1907 :param key: Key of the setting to return.
1904 1908 :type: key: Optional(str)
1905 1909
1906 1910 Example output:
1907 1911
1908 1912 .. code-block:: bash
1909 1913
1910 1914 {
1911 1915 "error": null,
1912 1916 "id": 237,
1913 1917 "result": {
1914 1918 "extensions_largefiles": true,
1915 1919 "extensions_evolve": true,
1916 1920 "hooks_changegroup_push_logger": true,
1917 1921 "hooks_changegroup_repo_size": false,
1918 1922 "hooks_outgoing_pull_logger": true,
1919 1923 "phases_publish": "True",
1920 1924 "rhodecode_hg_use_rebase_for_merging": true,
1921 1925 "rhodecode_pr_merge_enabled": true,
1922 1926 "rhodecode_use_outdated_comments": true
1923 1927 }
1924 1928 }
1925 1929 """
1926 1930
1927 1931 # Restrict access to this api method to admins only.
1928 1932 if not has_superadmin_permission(apiuser):
1929 1933 raise JSONRPCForbidden()
1930 1934
1931 1935 try:
1932 1936 repo = get_repo_or_error(repoid)
1933 1937 settings_model = VcsSettingsModel(repo=repo)
1934 1938 settings = settings_model.get_global_settings()
1935 1939 settings.update(settings_model.get_repo_settings())
1936 1940
1937 1941 # If only a single setting is requested fetch it from all settings.
1938 1942 key = Optional.extract(key)
1939 1943 if key is not None:
1940 1944 settings = settings.get(key, None)
1941 1945 except Exception:
1942 1946 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
1943 1947 log.exception(msg)
1944 1948 raise JSONRPCError(msg)
1945 1949
1946 1950 return settings
1947 1951
1948 1952
1949 1953 @jsonrpc_method()
1950 1954 def set_repo_settings(request, apiuser, repoid, settings):
1951 1955 """
1952 1956 Update repository settings. Returns true on success.
1953 1957
1954 1958 :param apiuser: This is filled automatically from the |authtoken|.
1955 1959 :type apiuser: AuthUser
1956 1960 :param repoid: The repository name or repository id.
1957 1961 :type repoid: str or int
1958 1962 :param settings: The new settings for the repository.
1959 1963 :type: settings: dict
1960 1964
1961 1965 Example output:
1962 1966
1963 1967 .. code-block:: bash
1964 1968
1965 1969 {
1966 1970 "error": null,
1967 1971 "id": 237,
1968 1972 "result": true
1969 1973 }
1970 1974 """
1971 1975 # Restrict access to this api method to admins only.
1972 1976 if not has_superadmin_permission(apiuser):
1973 1977 raise JSONRPCForbidden()
1974 1978
1975 1979 if type(settings) is not dict:
1976 1980 raise JSONRPCError('Settings have to be a JSON Object.')
1977 1981
1978 1982 try:
1979 1983 settings_model = VcsSettingsModel(repo=repoid)
1980 1984
1981 1985 # Merge global, repo and incoming settings.
1982 1986 new_settings = settings_model.get_global_settings()
1983 1987 new_settings.update(settings_model.get_repo_settings())
1984 1988 new_settings.update(settings)
1985 1989
1986 1990 # Update the settings.
1987 1991 inherit_global_settings = new_settings.get(
1988 1992 'inherit_global_settings', False)
1989 1993 settings_model.create_or_update_repo_settings(
1990 1994 new_settings, inherit_global_settings=inherit_global_settings)
1991 1995 Session().commit()
1992 1996 except Exception:
1993 1997 msg = 'Failed to update settings for repository `{}`'.format(repoid)
1994 1998 log.exception(msg)
1995 1999 raise JSONRPCError(msg)
1996 2000
1997 2001 # Indicate success.
1998 2002 return True
1999 2003
2000 2004
2001 2005 @jsonrpc_method()
2002 2006 def maintenance(request, apiuser, repoid):
2003 2007 """
2004 2008 Triggers a maintenance on the given repository.
2005 2009
2006 2010 This command can only be run using an |authtoken| with admin
2007 2011 rights to the specified repository. For more information,
2008 2012 see :ref:`config-token-ref`.
2009 2013
2010 2014 This command takes the following options:
2011 2015
2012 2016 :param apiuser: This is filled automatically from the |authtoken|.
2013 2017 :type apiuser: AuthUser
2014 2018 :param repoid: The repository name or repository ID.
2015 2019 :type repoid: str or int
2016 2020
2017 2021 Example output:
2018 2022
2019 2023 .. code-block:: bash
2020 2024
2021 2025 id : <id_given_in_input>
2022 2026 result : {
2023 2027 "msg": "executed maintenance command",
2024 2028 "executed_actions": [
2025 2029 <action_message>, <action_message2>...
2026 2030 ],
2027 2031 "repository": "<repository name>"
2028 2032 }
2029 2033 error : null
2030 2034
2031 2035 Example error output:
2032 2036
2033 2037 .. code-block:: bash
2034 2038
2035 2039 id : <id_given_in_input>
2036 2040 result : null
2037 2041 error : {
2038 2042 "Unable to execute maintenance on `<reponame>`"
2039 2043 }
2040 2044
2041 2045 """
2042 2046
2043 2047 repo = get_repo_or_error(repoid)
2044 2048 if not has_superadmin_permission(apiuser):
2045 2049 _perms = ('repository.admin',)
2046 2050 validate_repo_permissions(apiuser, repoid, repo, _perms)
2047 2051
2048 2052 try:
2049 2053 maintenance = repo_maintenance.RepoMaintenance()
2050 2054 executed_actions = maintenance.execute(repo)
2051 2055
2052 2056 return {
2053 2057 'msg': 'executed maintenance command',
2054 2058 'executed_actions': executed_actions,
2055 2059 'repository': repo.repo_name
2056 2060 }
2057 2061 except Exception:
2058 2062 log.exception("Exception occurred while trying to run maintenance")
2059 2063 raise JSONRPCError(
2060 2064 'Unable to execute maintenance on `%s`' % repo.repo_name)
@@ -1,430 +1,430 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_sync_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_sync_uri_validator,
323 323 preparers=[preparers.strip_preparer],
324 324 missing='')
325 325
326 326 repo_push_uri = colander.SchemaNode(
327 327 colander.String(),
328 validator=colander.All(colander.Length(min=1)),
328 validator=deferred_sync_uri_validator,
329 329 preparers=[preparers.strip_preparer],
330 330 missing='')
331 331
332 332 repo_fork_of = colander.SchemaNode(
333 333 colander.String(),
334 334 validator=deferred_fork_of_validator,
335 335 missing=None)
336 336
337 337 repo_private = colander.SchemaNode(
338 338 types.StringBooleanType(),
339 339 missing=False, widget=deform.widget.CheckboxWidget())
340 340 repo_copy_permissions = colander.SchemaNode(
341 341 types.StringBooleanType(),
342 342 missing=False, widget=deform.widget.CheckboxWidget())
343 343 repo_enable_statistics = colander.SchemaNode(
344 344 types.StringBooleanType(),
345 345 missing=False, widget=deform.widget.CheckboxWidget())
346 346 repo_enable_downloads = colander.SchemaNode(
347 347 types.StringBooleanType(),
348 348 missing=False, widget=deform.widget.CheckboxWidget())
349 349 repo_enable_locking = colander.SchemaNode(
350 350 types.StringBooleanType(),
351 351 missing=False, widget=deform.widget.CheckboxWidget())
352 352
353 353 def deserialize(self, cstruct):
354 354 """
355 355 Custom deserialize that allows to chain validation, and verify
356 356 permissions, and as last step uniqueness
357 357 """
358 358
359 359 # first pass, to validate given data
360 360 appstruct = super(RepoSchema, self).deserialize(cstruct)
361 361 validated_name = appstruct['repo_name']
362 362
363 363 # second pass to validate permissions to repo_group
364 364 second = RepoGroupAccessSchema().bind(**self.bindings)
365 365 appstruct_second = second.deserialize({'repo_group': validated_name})
366 366 # save result
367 367 appstruct['repo_group'] = appstruct_second['repo_group']
368 368
369 369 # thirds to validate uniqueness
370 370 third = RepoNameUniqueSchema().bind(**self.bindings)
371 371 third.deserialize({'unique_repo_name': validated_name})
372 372
373 373 return appstruct
374 374
375 375
376 376 class RepoSettingsSchema(RepoSchema):
377 377 repo_group = colander.SchemaNode(
378 378 colander.Integer(),
379 379 validator=deferred_repo_group_validator,
380 380 widget=deferred_repo_group_widget,
381 381 missing='')
382 382
383 383 repo_clone_uri_change = colander.SchemaNode(
384 384 colander.String(),
385 385 missing='NEW')
386 386
387 387 repo_clone_uri = colander.SchemaNode(
388 388 colander.String(),
389 389 preparers=[preparers.strip_preparer],
390 390 validator=deferred_sync_uri_validator,
391 391 missing='')
392 392
393 393 repo_push_uri_change = colander.SchemaNode(
394 394 colander.String(),
395 395 missing='NEW')
396 396
397 397 repo_push_uri = colander.SchemaNode(
398 398 colander.String(),
399 399 preparers=[preparers.strip_preparer],
400 400 validator=deferred_sync_uri_validator,
401 401 missing='')
402 402
403 403 def deserialize(self, cstruct):
404 404 """
405 405 Custom deserialize that allows to chain validation, and verify
406 406 permissions, and as last step uniqueness
407 407 """
408 408
409 409 # first pass, to validate given data
410 410 appstruct = super(RepoSchema, self).deserialize(cstruct)
411 411 validated_name = appstruct['repo_name']
412 412 # because of repoSchema adds repo-group as an ID, we inject it as
413 413 # full name here because validators require it, it's unwrapped later
414 414 # so it's safe to use and final name is going to be without group anyway
415 415
416 416 group, separator = get_repo_group(appstruct['repo_group'])
417 417 if group:
418 418 validated_name = separator.join([group.group_name, validated_name])
419 419
420 420 # second pass to validate permissions to repo_group
421 421 second = RepoGroupAccessSchema().bind(**self.bindings)
422 422 appstruct_second = second.deserialize({'repo_group': validated_name})
423 423 # save result
424 424 appstruct['repo_group'] = appstruct_second['repo_group']
425 425
426 426 # thirds to validate uniqueness
427 427 third = RepoNameUniqueSchema().bind(**self.bindings)
428 428 third.deserialize({'unique_repo_name': validated_name})
429 429
430 430 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