##// END OF EJS Templates
gists: use colander schema to validate input data....
marcink -
r523:878882bd default
parent child Browse files
Show More
@@ -0,0 +1,24 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import colander
22
23 from colander import Invalid # noqa, don't remove this
24
@@ -0,0 +1,89 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import unicodedata
22
23
24
25 def strip_preparer(value):
26 """
27 strips given values using .strip() function
28 """
29
30 if value:
31 value = value.strip()
32 return value
33
34
35 def slugify_preparer(value):
36 """
37 Slugify given value to a safe representation for url/id
38 """
39 from rhodecode.lib.utils import repo_name_slug
40 if value:
41 value = repo_name_slug(value.lower())
42 return value
43
44
45 def non_ascii_strip_preparer(value):
46 """
47 trie to replace non-ascii letters to their ascii representation
48 eg::
49
50 `ΕΌoΕ‚w` converts into `zolw`
51 """
52 if value:
53 value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')
54 return value
55
56
57 def unique_list_preparer(value):
58 """
59 Converts an list to a list with only unique values
60 """
61
62 def make_unique(value):
63 seen = []
64 return [c for c in value if
65 not (c in seen or seen.append(c))]
66
67 if isinstance(value, list):
68 ret_val = make_unique(value)
69 elif isinstance(value, set):
70 ret_val = list(value)
71 elif isinstance(value, tuple):
72 ret_val = make_unique(value)
73 elif value is None:
74 ret_val = []
75 else:
76 ret_val = [value]
77
78 return ret_val
79
80
81 def unique_list_from_str_preparer(value):
82 """
83 Converts an list to a list with only unique values
84 """
85 from rhodecode.lib.utils2 import aslist
86
87 if isinstance(value, basestring):
88 value = aslist(value, ',')
89 return unique_list_preparer(value) No newline at end of file
@@ -0,0 +1,25 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 """
22 Colander Schema nodes
23 http://docs.pylonsproject.org/projects/colander/en/latest/basics.html#schema-node-objects
24 """
25
@@ -0,0 +1,185 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import os
22
23 import colander
24
25 from rhodecode.translation import _
26 from rhodecode.model.validation_schema import validators, preparers
27
28
29 def nodes_to_sequence(nodes, colander_node=None):
30 """
31 Converts old style dict nodes to new list of dicts
32
33 :param nodes: dict with key beeing name of the file
34
35 """
36 if not isinstance(nodes, dict):
37 msg = 'Nodes needs to be a dict, got {}'.format(type(nodes))
38 raise colander.Invalid(colander_node, msg)
39 out = []
40
41 for key, val in nodes.items():
42 val = (isinstance(val, dict) and val) or {}
43 out.append(dict(
44 filename=key,
45 content=val.get('content'),
46 mimetype=val.get('mimetype')
47 ))
48
49 out = Nodes().deserialize(out)
50 return out
51
52
53 def sequence_to_nodes(nodes, colander_node=None):
54 if not isinstance(nodes, list):
55 msg = 'Nodes needs to be a list, got {}'.format(type(nodes))
56 raise colander.Invalid(colander_node, msg)
57 nodes = Nodes().deserialize(nodes)
58
59 out = {}
60 try:
61 for file_data in nodes:
62 file_data_skip = file_data.copy()
63 # if we got filename_org we use it as a key so we keep old
64 # name as input and rename is-reflected inside the values as
65 # filename and filename_org differences.
66 filename_org = file_data.get('filename_org')
67 filename = filename_org or file_data['filename']
68 out[filename] = {}
69 out[filename].update(file_data_skip)
70
71 except Exception as e:
72 msg = 'Invalid data format org_exc:`{}`'.format(repr(e))
73 raise colander.Invalid(colander_node, msg)
74 return out
75
76
77 @colander.deferred
78 def deferred_lifetime_validator(node, kw):
79 options = kw.get('lifetime_options', [])
80 return colander.All(
81 colander.Range(min=-1, max=60 * 24 * 30 * 12),
82 colander.OneOf([x for x in options]))
83
84
85 def unique_gist_validator(node, value):
86 from rhodecode.model.db import Gist
87 existing = Gist.get_by_access_id(value)
88 if existing:
89 msg = _(u'Gist with name {} already exists').format(value)
90 raise colander.Invalid(node, msg)
91
92
93 def filename_validator(node, value):
94 if value != os.path.basename(value):
95 msg = _(u'Filename {} cannot be inside a directory').format(value)
96 raise colander.Invalid(node, msg)
97
98
99 class NodeSchema(colander.MappingSchema):
100 # if we perform rename this will be org filename
101 filename_org = colander.SchemaNode(
102 colander.String(),
103 preparer=[preparers.strip_preparer,
104 preparers.non_ascii_strip_preparer],
105 validator=filename_validator,
106 missing=None)
107
108 filename = colander.SchemaNode(
109 colander.String(),
110 preparer=[preparers.strip_preparer,
111 preparers.non_ascii_strip_preparer],
112 validator=filename_validator)
113
114 content = colander.SchemaNode(
115 colander.String())
116 mimetype = colander.SchemaNode(
117 colander.String(),
118 missing=None)
119
120
121 class Nodes(colander.SequenceSchema):
122 filenames = NodeSchema()
123
124 def validator(self, node, cstruct):
125 if not isinstance(cstruct, list):
126 return
127
128 found_filenames = []
129 for data in cstruct:
130 filename = data['filename']
131 if filename in found_filenames:
132 msg = _('Duplicated value for filename found: `{}`').format(
133 filename)
134 raise colander.Invalid(node, msg)
135 found_filenames.append(filename)
136
137
138 class GistSchema(colander.MappingSchema):
139 """
140 schema = GistSchema()
141 schema.bind(
142 lifetime_options = [1,2,3]
143 )
144 out = schema.deserialize(dict(
145 nodes=[
146 {'filename': 'x', 'content': 'xxx', },
147 {'filename': 'docs/Z', 'content': 'xxx', 'mimetype': 'x'},
148 ]
149 ))
150 """
151
152 from rhodecode.model.db import Gist
153
154 gistid = colander.SchemaNode(
155 colander.String(),
156 missing=None,
157 preparer=[preparers.strip_preparer,
158 preparers.non_ascii_strip_preparer,
159 preparers.slugify_preparer],
160 validator=colander.All(
161 colander.Length(min=3),
162 unique_gist_validator
163 ))
164
165 description = colander.SchemaNode(
166 colander.String(),
167 missing=u'')
168
169 lifetime = colander.SchemaNode(
170 colander.Integer(),
171 validator=deferred_lifetime_validator)
172
173 gist_acl_level = colander.SchemaNode(
174 colander.String(),
175 validator=colander.OneOf([Gist.ACL_LEVEL_PUBLIC,
176 Gist.ACL_LEVEL_PRIVATE]))
177
178 gist_type = colander.SchemaNode(
179 colander.String(),
180 missing=Gist.ACL_LEVEL_PUBLIC,
181 validator=colander.OneOf([Gist.GIST_PRIVATE, Gist.GIST_PUBLIC]))
182
183 nodes = Nodes()
184
185
@@ -0,0 +1,29 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21
22 import colander
23
24
25 from rhodecode.model.validation_schema import validators, preparers, types
26
27
28 class RepoGroupSchema(colander.Schema):
29 group_name = colander.SchemaNode(types.GroupNameType())
@@ -0,0 +1,27 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import colander
22
23 from rhodecode.model.validation_schema import validators, preparers, types
24
25
26 class RepoSchema(colander.Schema):
27 repo_name = colander.SchemaNode(types.GroupNameType())
@@ -0,0 +1,44 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21
22 import colander
23
24
25 class SearchParamsSchema(colander.MappingSchema):
26 search_query = colander.SchemaNode(
27 colander.String(),
28 missing='')
29 search_type = colander.SchemaNode(
30 colander.String(),
31 missing='content',
32 validator=colander.OneOf(['content', 'path', 'commit', 'repository']))
33 search_sort = colander.SchemaNode(
34 colander.String(),
35 missing='newfirst',
36 validator=colander.OneOf(
37 ['oldfirst', 'newfirst']))
38 page_limit = colander.SchemaNode(
39 colander.Integer(),
40 missing=10,
41 validator=colander.Range(1, 500))
42 requested_page = colander.SchemaNode(
43 colander.Integer(),
44 missing=1)
@@ -0,0 +1,34 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import colander
22
23
24 class GroupNameType(colander.String):
25 SEPARATOR = '/'
26
27 def deserialize(self, node, cstruct):
28 result = super(GroupNameType, self).deserialize(node, cstruct)
29 return self._replace_extra_slashes(result)
30
31 def _replace_extra_slashes(self, path):
32 path = path.split(self.SEPARATOR)
33 path = [item for item in path if item]
34 return self.SEPARATOR.join(path)
@@ -0,0 +1,19 b''
1 import os
2
3 import ipaddress
4 import colander
5
6 from rhodecode.translation import _
7
8
9 def ip_addr_validator(node, value):
10 try:
11 # this raises an ValueError if address is not IpV4 or IpV6
12 ipaddress.ip_network(value, strict=False)
13 except ValueError:
14 msg = _(u'Please enter a valid IPv4 or IpV6 address')
15 raise colander.Invalid(node, msg)
16
17
18
19
1 NO CONTENT: new file 100644
@@ -0,0 +1,100 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import colander
22 import pytest
23
24 from rhodecode.model import validation_schema
25 from rhodecode.model.validation_schema.schemas import gist_schema
26
27
28 class TestGistSchema(object):
29
30 def test_deserialize_bad_data(self):
31 schema = gist_schema.GistSchema().bind(
32 lifetime_options=[1, 2, 3]
33 )
34 with pytest.raises(validation_schema.Invalid) as exc_info:
35 schema.deserialize('err')
36 err = exc_info.value.asdict()
37 assert err[''] == '"err" is not a mapping type: ' \
38 'Does not implement dict-like functionality.'
39
40 def test_deserialize_bad_lifetime_options(self):
41 schema = gist_schema.GistSchema().bind(
42 lifetime_options=[1, 2, 3]
43 )
44 with pytest.raises(validation_schema.Invalid) as exc_info:
45 schema.deserialize(dict(
46 lifetime=10
47 ))
48 err = exc_info.value.asdict()
49 assert err['lifetime'] == '"10" is not one of 1, 2, 3'
50
51 with pytest.raises(validation_schema.Invalid) as exc_info:
52 schema.deserialize(dict(
53 lifetime='x'
54 ))
55 err = exc_info.value.asdict()
56 assert err['lifetime'] == '"x" is not a number'
57
58 def test_serialize_data_correctly(self):
59 schema = gist_schema.GistSchema().bind(
60 lifetime_options=[1, 2, 3]
61 )
62 nodes = [{
63 'filename': 'foobar',
64 'filename_org': 'foobar',
65 'content': 'content',
66 'mimetype': 'xx'
67 }]
68 schema_data = schema.deserialize(dict(
69 lifetime=2,
70 gist_type='public',
71 gist_acl_level='acl_public',
72 nodes=nodes,
73 ))
74
75 assert schema_data['nodes'] == nodes
76
77 def test_serialize_data_correctly_with_conversion(self):
78 schema = gist_schema.GistSchema().bind(
79 lifetime_options=[1, 2, 3],
80 convert_nodes=True
81 )
82 nodes = [{
83 'filename': 'foobar',
84 'filename_org': None,
85 'content': 'content',
86 'mimetype': 'xx'
87 }]
88 schema_data = schema.deserialize(dict(
89 lifetime=2,
90 gist_type='public',
91 gist_acl_level='acl_public',
92 nodes=nodes,
93 ))
94
95 assert schema_data['nodes'] == nodes
96
97 seq_nodes = gist_schema.sequence_to_nodes(nodes)
98 assert isinstance(seq_nodes, dict)
99 seq_nodes = gist_schema.nodes_to_sequence(seq_nodes)
100 assert nodes == seq_nodes
@@ -1,497 +1,503 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2016 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 inspect
22 22 import itertools
23 23 import logging
24 24 import types
25 25
26 26 import decorator
27 27 import venusian
28 28 from pyramid.exceptions import ConfigurationError
29 29 from pyramid.renderers import render
30 30 from pyramid.response import Response
31 31 from pyramid.httpexceptions import HTTPNotFound
32 32
33 from rhodecode.api.exc import JSONRPCBaseError, JSONRPCError, JSONRPCForbidden
33 from rhodecode.api.exc import (
34 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
34 35 from rhodecode.lib.auth import AuthUser
35 36 from rhodecode.lib.base import get_ip_addr
36 37 from rhodecode.lib.ext_json import json
37 38 from rhodecode.lib.utils2 import safe_str
38 39 from rhodecode.lib.plugins.utils import get_plugin_settings
39 40 from rhodecode.model.db import User, UserApiKeys
40 41
41 42 log = logging.getLogger(__name__)
42 43
43 44 DEFAULT_RENDERER = 'jsonrpc_renderer'
44 45 DEFAULT_URL = '/_admin/apiv2'
45 46
46 47
47 48 class ExtJsonRenderer(object):
48 49 """
49 50 Custom renderer that mkaes use of our ext_json lib
50 51
51 52 """
52 53
53 54 def __init__(self, serializer=json.dumps, **kw):
54 55 """ Any keyword arguments will be passed to the ``serializer``
55 56 function."""
56 57 self.serializer = serializer
57 58 self.kw = kw
58 59
59 60 def __call__(self, info):
60 61 """ Returns a plain JSON-encoded string with content-type
61 62 ``application/json``. The content-type may be overridden by
62 63 setting ``request.response.content_type``."""
63 64
64 65 def _render(value, system):
65 66 request = system.get('request')
66 67 if request is not None:
67 68 response = request.response
68 69 ct = response.content_type
69 70 if ct == response.default_content_type:
70 71 response.content_type = 'application/json'
71 72
72 73 return self.serializer(value, **self.kw)
73 74
74 75 return _render
75 76
76 77
77 78 def jsonrpc_response(request, result):
78 79 rpc_id = getattr(request, 'rpc_id', None)
79 80 response = request.response
80 81
81 82 # store content_type before render is called
82 83 ct = response.content_type
83 84
84 85 ret_value = ''
85 86 if rpc_id:
86 87 ret_value = {
87 88 'id': rpc_id,
88 89 'result': result,
89 90 'error': None,
90 91 }
91 92
92 93 # fetch deprecation warnings, and store it inside results
93 94 deprecation = getattr(request, 'rpc_deprecation', None)
94 95 if deprecation:
95 96 ret_value['DEPRECATION_WARNING'] = deprecation
96 97
97 98 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
98 99 response.body = safe_str(raw_body, response.charset)
99 100
100 101 if ct == response.default_content_type:
101 102 response.content_type = 'application/json'
102 103
103 104 return response
104 105
105 106
106 107 def jsonrpc_error(request, message, retid=None, code=None):
107 108 """
108 109 Generate a Response object with a JSON-RPC error body
109 110
110 111 :param code:
111 112 :param retid:
112 113 :param message:
113 114 """
114 115 err_dict = {'id': retid, 'result': None, 'error': message}
115 116 body = render(DEFAULT_RENDERER, err_dict, request=request).encode('utf-8')
116 117 return Response(
117 118 body=body,
118 119 status=code,
119 120 content_type='application/json'
120 121 )
121 122
122 123
123 124 def exception_view(exc, request):
124 125 rpc_id = getattr(request, 'rpc_id', None)
125 126
126 127 fault_message = 'undefined error'
127 128 if isinstance(exc, JSONRPCError):
128 129 fault_message = exc.message
129 130 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
131 elif isinstance(exc, JSONRPCValidationError):
132 colander_exc = exc.colander_exception
133 #TODO: think maybe of nicer way to serialize errors ?
134 fault_message = colander_exc.asdict()
135 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
130 136 elif isinstance(exc, JSONRPCForbidden):
131 137 fault_message = 'Access was denied to this resource.'
132 138 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
133 139 elif isinstance(exc, HTTPNotFound):
134 140 method = request.rpc_method
135 141 log.debug('json-rpc method `%s` not found in list of '
136 142 'api calls: %s, rpc_id:%s',
137 143 method, request.registry.jsonrpc_methods.keys(), rpc_id)
138 144 fault_message = "No such method: {}".format(method)
139 145
140 146 return jsonrpc_error(request, fault_message, rpc_id)
141 147
142 148
143 149 def request_view(request):
144 150 """
145 151 Main request handling method. It handles all logic to call a specific
146 152 exposed method
147 153 """
148 154
149 155 # check if we can find this session using api_key, get_by_auth_token
150 156 # search not expired tokens only
151 157
152 158 try:
153 159 u = User.get_by_auth_token(request.rpc_api_key)
154 160
155 161 if u is None:
156 162 return jsonrpc_error(
157 163 request, retid=request.rpc_id, message='Invalid API KEY')
158 164
159 165 if not u.active:
160 166 return jsonrpc_error(
161 167 request, retid=request.rpc_id,
162 168 message='Request from this user not allowed')
163 169
164 170 # check if we are allowed to use this IP
165 171 auth_u = AuthUser(
166 172 u.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
167 173 if not auth_u.ip_allowed:
168 174 return jsonrpc_error(
169 175 request, retid=request.rpc_id,
170 176 message='Request from IP:%s not allowed' % (
171 177 request.rpc_ip_addr,))
172 178 else:
173 179 log.info('Access for IP:%s allowed' % (request.rpc_ip_addr,))
174 180
175 181 # now check if token is valid for API
176 182 role = UserApiKeys.ROLE_API
177 183 extra_auth_tokens = [
178 184 x.api_key for x in User.extra_valid_auth_tokens(u, role=role)]
179 185 active_tokens = [u.api_key] + extra_auth_tokens
180 186
181 187 log.debug('Checking if API key has proper role')
182 188 if request.rpc_api_key not in active_tokens:
183 189 return jsonrpc_error(
184 190 request, retid=request.rpc_id,
185 191 message='API KEY has bad role for an API call')
186 192
187 193 except Exception as e:
188 194 log.exception('Error on API AUTH')
189 195 return jsonrpc_error(
190 196 request, retid=request.rpc_id, message='Invalid API KEY')
191 197
192 198 method = request.rpc_method
193 199 func = request.registry.jsonrpc_methods[method]
194 200
195 201 # now that we have a method, add request._req_params to
196 202 # self.kargs and dispatch control to WGIController
197 203 argspec = inspect.getargspec(func)
198 204 arglist = argspec[0]
199 205 defaults = map(type, argspec[3] or [])
200 206 default_empty = types.NotImplementedType
201 207
202 208 # kw arguments required by this method
203 209 func_kwargs = dict(itertools.izip_longest(
204 210 reversed(arglist), reversed(defaults), fillvalue=default_empty))
205 211
206 212 # This attribute will need to be first param of a method that uses
207 213 # api_key, which is translated to instance of user at that name
208 214 user_var = 'apiuser'
209 215 request_var = 'request'
210 216
211 217 for arg in [user_var, request_var]:
212 218 if arg not in arglist:
213 219 return jsonrpc_error(
214 220 request,
215 221 retid=request.rpc_id,
216 222 message='This method [%s] does not support '
217 223 'required parameter `%s`' % (func.__name__, arg))
218 224
219 225 # get our arglist and check if we provided them as args
220 226 for arg, default in func_kwargs.items():
221 227 if arg in [user_var, request_var]:
222 228 # user_var and request_var are pre-hardcoded parameters and we
223 229 # don't need to do any translation
224 230 continue
225 231
226 232 # skip the required param check if it's default value is
227 233 # NotImplementedType (default_empty)
228 234 if default == default_empty and arg not in request.rpc_params:
229 235 return jsonrpc_error(
230 236 request,
231 237 retid=request.rpc_id,
232 238 message=('Missing non optional `%s` arg in JSON DATA' % arg)
233 239 )
234 240
235 241 # sanitze extra passed arguments
236 242 for k in request.rpc_params.keys()[:]:
237 243 if k not in func_kwargs:
238 244 del request.rpc_params[k]
239 245
240 246 call_params = request.rpc_params
241 247 call_params.update({
242 248 'request': request,
243 249 'apiuser': auth_u
244 250 })
245 251 try:
246 252 ret_value = func(**call_params)
247 253 return jsonrpc_response(request, ret_value)
248 254 except JSONRPCBaseError:
249 255 raise
250 256 except Exception:
251 257 log.exception('Unhandled exception occured on api call: %s', func)
252 258 return jsonrpc_error(request, retid=request.rpc_id,
253 259 message='Internal server error')
254 260
255 261
256 262 def setup_request(request):
257 263 """
258 264 Parse a JSON-RPC request body. It's used inside the predicates method
259 265 to validate and bootstrap requests for usage in rpc calls.
260 266
261 267 We need to raise JSONRPCError here if we want to return some errors back to
262 268 user.
263 269 """
264 270 log.debug('Executing setup request: %r', request)
265 271 request.rpc_ip_addr = get_ip_addr(request.environ)
266 272 # TODO: marcink, deprecate GET at some point
267 273 if request.method not in ['POST', 'GET']:
268 274 log.debug('unsupported request method "%s"', request.method)
269 275 raise JSONRPCError(
270 276 'unsupported request method "%s". Please use POST' % request.method)
271 277
272 278 if 'CONTENT_LENGTH' not in request.environ:
273 279 log.debug("No Content-Length")
274 280 raise JSONRPCError("Empty body, No Content-Length in request")
275 281
276 282 else:
277 283 length = request.environ['CONTENT_LENGTH']
278 284 log.debug('Content-Length: %s', length)
279 285
280 286 if length == 0:
281 287 log.debug("Content-Length is 0")
282 288 raise JSONRPCError("Content-Length is 0")
283 289
284 290 raw_body = request.body
285 291 try:
286 292 json_body = json.loads(raw_body)
287 293 except ValueError as e:
288 294 # catch JSON errors Here
289 295 raise JSONRPCError("JSON parse error ERR:%s RAW:%r" % (e, raw_body))
290 296
291 297 request.rpc_id = json_body.get('id')
292 298 request.rpc_method = json_body.get('method')
293 299
294 300 # check required base parameters
295 301 try:
296 302 api_key = json_body.get('api_key')
297 303 if not api_key:
298 304 api_key = json_body.get('auth_token')
299 305
300 306 if not api_key:
301 307 raise KeyError('api_key or auth_token')
302 308
303 309 request.rpc_api_key = api_key
304 310 request.rpc_id = json_body['id']
305 311 request.rpc_method = json_body['method']
306 312 request.rpc_params = json_body['args'] \
307 313 if isinstance(json_body['args'], dict) else {}
308 314
309 315 log.debug(
310 316 'method: %s, params: %s' % (request.rpc_method, request.rpc_params))
311 317 except KeyError as e:
312 318 raise JSONRPCError('Incorrect JSON data. Missing %s' % e)
313 319
314 320 log.debug('setup complete, now handling method:%s rpcid:%s',
315 321 request.rpc_method, request.rpc_id, )
316 322
317 323
318 324 class RoutePredicate(object):
319 325 def __init__(self, val, config):
320 326 self.val = val
321 327
322 328 def text(self):
323 329 return 'jsonrpc route = %s' % self.val
324 330
325 331 phash = text
326 332
327 333 def __call__(self, info, request):
328 334 if self.val:
329 335 # potentially setup and bootstrap our call
330 336 setup_request(request)
331 337
332 338 # Always return True so that even if it isn't a valid RPC it
333 339 # will fall through to the underlaying handlers like notfound_view
334 340 return True
335 341
336 342
337 343 class NotFoundPredicate(object):
338 344 def __init__(self, val, config):
339 345 self.val = val
340 346
341 347 def text(self):
342 348 return 'jsonrpc method not found = %s' % self.val
343 349
344 350 phash = text
345 351
346 352 def __call__(self, info, request):
347 353 return hasattr(request, 'rpc_method')
348 354
349 355
350 356 class MethodPredicate(object):
351 357 def __init__(self, val, config):
352 358 self.method = val
353 359
354 360 def text(self):
355 361 return 'jsonrpc method = %s' % self.method
356 362
357 363 phash = text
358 364
359 365 def __call__(self, context, request):
360 366 # we need to explicitly return False here, so pyramid doesn't try to
361 367 # execute our view directly. We need our main handler to execute things
362 368 return getattr(request, 'rpc_method') == self.method
363 369
364 370
365 371 def add_jsonrpc_method(config, view, **kwargs):
366 372 # pop the method name
367 373 method = kwargs.pop('method', None)
368 374
369 375 if method is None:
370 376 raise ConfigurationError(
371 377 'Cannot register a JSON-RPC method without specifying the '
372 378 '"method"')
373 379
374 380 # we define custom predicate, to enable to detect conflicting methods,
375 381 # those predicates are kind of "translation" from the decorator variables
376 382 # to internal predicates names
377 383
378 384 kwargs['jsonrpc_method'] = method
379 385
380 386 # register our view into global view store for validation
381 387 config.registry.jsonrpc_methods[method] = view
382 388
383 389 # we're using our main request_view handler, here, so each method
384 390 # has a unified handler for itself
385 391 config.add_view(request_view, route_name='apiv2', **kwargs)
386 392
387 393
388 394 class jsonrpc_method(object):
389 395 """
390 396 decorator that works similar to @add_view_config decorator,
391 397 but tailored for our JSON RPC
392 398 """
393 399
394 400 venusian = venusian # for testing injection
395 401
396 402 def __init__(self, method=None, **kwargs):
397 403 self.method = method
398 404 self.kwargs = kwargs
399 405
400 406 def __call__(self, wrapped):
401 407 kwargs = self.kwargs.copy()
402 408 kwargs['method'] = self.method or wrapped.__name__
403 409 depth = kwargs.pop('_depth', 0)
404 410
405 411 def callback(context, name, ob):
406 412 config = context.config.with_package(info.module)
407 413 config.add_jsonrpc_method(view=ob, **kwargs)
408 414
409 415 info = venusian.attach(wrapped, callback, category='pyramid',
410 416 depth=depth + 1)
411 417 if info.scope == 'class':
412 418 # ensure that attr is set if decorating a class method
413 419 kwargs.setdefault('attr', wrapped.__name__)
414 420
415 421 kwargs['_info'] = info.codeinfo # fbo action_method
416 422 return wrapped
417 423
418 424
419 425 class jsonrpc_deprecated_method(object):
420 426 """
421 427 Marks method as deprecated, adds log.warning, and inject special key to
422 428 the request variable to mark method as deprecated.
423 429 Also injects special docstring that extract_docs will catch to mark
424 430 method as deprecated.
425 431
426 432 :param use_method: specify which method should be used instead of
427 433 the decorated one
428 434
429 435 Use like::
430 436
431 437 @jsonrpc_method()
432 438 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
433 439 def old_func(request, apiuser, arg1, arg2):
434 440 ...
435 441 """
436 442
437 443 def __init__(self, use_method, deprecated_at_version):
438 444 self.use_method = use_method
439 445 self.deprecated_at_version = deprecated_at_version
440 446 self.deprecated_msg = ''
441 447
442 448 def __call__(self, func):
443 449 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
444 450 method=self.use_method)
445 451
446 452 docstring = """\n
447 453 .. deprecated:: {version}
448 454
449 455 {deprecation_message}
450 456
451 457 {original_docstring}
452 458 """
453 459 func.__doc__ = docstring.format(
454 460 version=self.deprecated_at_version,
455 461 deprecation_message=self.deprecated_msg,
456 462 original_docstring=func.__doc__)
457 463 return decorator.decorator(self.__wrapper, func)
458 464
459 465 def __wrapper(self, func, *fargs, **fkwargs):
460 466 log.warning('DEPRECATED API CALL on function %s, please '
461 467 'use `%s` instead', func, self.use_method)
462 468 # alter function docstring to mark as deprecated, this is picked up
463 469 # via fabric file that generates API DOC.
464 470 result = func(*fargs, **fkwargs)
465 471
466 472 request = fargs[0]
467 473 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
468 474 return result
469 475
470 476
471 477 def includeme(config):
472 478 plugin_module = 'rhodecode.api'
473 479 plugin_settings = get_plugin_settings(
474 480 plugin_module, config.registry.settings)
475 481
476 482 if not hasattr(config.registry, 'jsonrpc_methods'):
477 483 config.registry.jsonrpc_methods = {}
478 484
479 485 # match filter by given method only
480 486 config.add_view_predicate(
481 487 'jsonrpc_method', MethodPredicate)
482 488
483 489 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
484 490 serializer=json.dumps, indent=4))
485 491 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
486 492
487 493 config.add_route_predicate(
488 494 'jsonrpc_call', RoutePredicate)
489 495
490 496 config.add_route(
491 497 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
492 498
493 499 config.scan(plugin_module, ignore='rhodecode.api.tests')
494 500 # register some exception handling view
495 501 config.add_view(exception_view, context=JSONRPCBaseError)
496 502 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
497 503 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
@@ -1,31 +1,39 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 class JSONRPCBaseError(Exception):
23 23 pass
24 24
25 25
26 26 class JSONRPCError(JSONRPCBaseError):
27 27 pass
28 28
29 29
30 class JSONRPCValidationError(JSONRPCBaseError):
31
32 def __init__(self, *args, **kwargs):
33 self.colander_exception = kwargs.pop('colander_exc')
34 super(JSONRPCValidationError, self).__init__(*args, **kwargs)
35
36
30 37 class JSONRPCForbidden(JSONRPCBaseError):
31 38 pass
39
@@ -1,76 +1,102 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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.db import Gist
25 25 from rhodecode.model.gist import GistModel
26 26 from rhodecode.api.tests.utils import (
27 27 build_data, api_call, assert_error, assert_ok, crash)
28 28 from rhodecode.tests.fixture import Fixture
29 29
30 30
31 31 @pytest.mark.usefixtures("testuser_api", "app")
32 32 class TestApiCreateGist(object):
33 33 @pytest.mark.parametrize("lifetime, gist_type, gist_acl_level", [
34 34 (10, Gist.GIST_PUBLIC, Gist.ACL_LEVEL_PUBLIC),
35 35 (20, Gist.GIST_PUBLIC, Gist.ACL_LEVEL_PRIVATE),
36 36 (40, Gist.GIST_PRIVATE, Gist.ACL_LEVEL_PUBLIC),
37 37 (80, Gist.GIST_PRIVATE, Gist.ACL_LEVEL_PRIVATE),
38 38 ])
39 39 def test_api_create_gist(self, lifetime, gist_type, gist_acl_level):
40 40 id_, params = build_data(
41 41 self.apikey_regular, 'create_gist',
42 42 lifetime=lifetime,
43 43 description='foobar-gist',
44 44 gist_type=gist_type,
45 45 acl_level=gist_acl_level,
46 files={'foobar': {'content': 'foo'}})
46 files={'foobar_Δ…Δ‡': {'content': 'foo'}})
47 47 response = api_call(self.app, params)
48 48 response_json = response.json
49 49 gist = response_json['result']['gist']
50 50 expected = {
51 51 'gist': {
52 52 'access_id': gist['access_id'],
53 53 'created_on': gist['created_on'],
54 54 'modified_at': gist['modified_at'],
55 55 'description': 'foobar-gist',
56 56 'expires': gist['expires'],
57 57 'gist_id': gist['gist_id'],
58 58 'type': gist_type,
59 59 'url': gist['url'],
60 60 # content is empty since we don't show it here
61 61 'content': None,
62 62 'acl_level': gist_acl_level,
63 63 },
64 64 'msg': 'created new gist'
65 65 }
66 66 try:
67 67 assert_ok(id_, expected, given=response.body)
68 68 finally:
69 69 Fixture().destroy_gists()
70 70
71 @pytest.mark.parametrize("expected, lifetime, gist_type, gist_acl_level, files", [
72 ({'gist_type': '"ups" is not one of private, public'},
73 10, 'ups', Gist.ACL_LEVEL_PUBLIC, {'f': {'content': 'f'}}),
74
75 ({'lifetime': '-120 is less than minimum value -1'},
76 -120, Gist.GIST_PUBLIC, Gist.ACL_LEVEL_PUBLIC, {'f': {'content': 'f'}}),
77
78 ({'0.content': 'Required'},
79 10, Gist.GIST_PUBLIC, Gist.ACL_LEVEL_PUBLIC, {'f': {'x': 'f'}}),
80 ])
81 def test_api_try_create_gist(
82 self, expected, lifetime, gist_type, gist_acl_level, files):
83 id_, params = build_data(
84 self.apikey_regular, 'create_gist',
85 lifetime=lifetime,
86 description='foobar-gist',
87 gist_type=gist_type,
88 acl_level=gist_acl_level,
89 files=files)
90 response = api_call(self.app, params)
91
92 try:
93 assert_error(id_, expected, given=response.body)
94 finally:
95 Fixture().destroy_gists()
96
71 97 @mock.patch.object(GistModel, 'create', crash)
72 98 def test_api_create_gist_exception_occurred(self):
73 99 id_, params = build_data(self.apikey_regular, 'create_gist', files={})
74 100 response = api_call(self.app, params)
75 101 expected = 'failed to create gist'
76 102 assert_error(id_, expected, given=response.body)
@@ -1,378 +1,376 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 JSON RPC utils
23 23 """
24 24
25 25 import collections
26 26 import logging
27 27
28 28 from rhodecode.api.exc import JSONRPCError
29 29 from rhodecode.lib.auth import HasPermissionAnyApi, HasRepoPermissionAnyApi
30 30 from rhodecode.lib.utils import safe_unicode
31 31 from rhodecode.controllers.utils import get_commit_from_ref_name
32 32 from rhodecode.lib.vcs.exceptions import RepositoryError
33 33
34 34 log = logging.getLogger(__name__)
35 35
36 36
37
38
39 37 class OAttr(object):
40 38 """
41 39 Special Option that defines other attribute, and can default to them
42 40
43 41 Example::
44 42
45 43 def test(apiuser, userid=Optional(OAttr('apiuser')):
46 44 user = Optional.extract(userid, evaluate_locals=local())
47 45 #if we pass in userid, we get it, else it will default to apiuser
48 46 #attribute
49 47 """
50 48
51 49 def __init__(self, attr_name):
52 50 self.attr_name = attr_name
53 51
54 52 def __repr__(self):
55 53 return '<OptionalAttr:%s>' % self.attr_name
56 54
57 55 def __call__(self):
58 56 return self
59 57
60 58
61 59 class Optional(object):
62 60 """
63 61 Defines an optional parameter::
64 62
65 63 param = param.getval() if isinstance(param, Optional) else param
66 64 param = param() if isinstance(param, Optional) else param
67 65
68 66 is equivalent of::
69 67
70 68 param = Optional.extract(param)
71 69
72 70 """
73 71
74 72 def __init__(self, type_):
75 73 self.type_ = type_
76 74
77 75 def __repr__(self):
78 76 return '<Optional:%s>' % self.type_.__repr__()
79 77
80 78 def __call__(self):
81 79 return self.getval()
82 80
83 81 def getval(self, evaluate_locals=None):
84 82 """
85 83 returns value from this Optional instance
86 84 """
87 85 if isinstance(self.type_, OAttr):
88 86 param_name = self.type_.attr_name
89 87 if evaluate_locals:
90 88 return evaluate_locals[param_name]
91 89 # use params name
92 90 return param_name
93 91 return self.type_
94 92
95 93 @classmethod
96 94 def extract(cls, val, evaluate_locals=None):
97 95 """
98 96 Extracts value from Optional() instance
99 97
100 98 :param val:
101 99 :return: original value if it's not Optional instance else
102 100 value of instance
103 101 """
104 102 if isinstance(val, cls):
105 103 return val.getval(evaluate_locals)
106 104 return val
107 105
108 106
109 107 def parse_args(cli_args, key_prefix=''):
110 108 from rhodecode.lib.utils2 import (escape_split)
111 109 kwargs = collections.defaultdict(dict)
112 110 for el in escape_split(cli_args, ','):
113 111 kv = escape_split(el, '=', 1)
114 112 if len(kv) == 2:
115 113 k, v = kv
116 114 kwargs[key_prefix + k] = v
117 115 return kwargs
118 116
119 117
120 118 def get_origin(obj):
121 119 """
122 120 Get origin of permission from object.
123 121
124 122 :param obj:
125 123 """
126 124 origin = 'permission'
127 125
128 126 if getattr(obj, 'owner_row', '') and getattr(obj, 'admin_row', ''):
129 127 # admin and owner case, maybe we should use dual string ?
130 128 origin = 'owner'
131 129 elif getattr(obj, 'owner_row', ''):
132 130 origin = 'owner'
133 131 elif getattr(obj, 'admin_row', ''):
134 132 origin = 'super-admin'
135 133 return origin
136 134
137 135
138 136 def store_update(updates, attr, name):
139 137 """
140 138 Stores param in updates dict if it's not instance of Optional
141 139 allows easy updates of passed in params
142 140 """
143 141 if not isinstance(attr, Optional):
144 142 updates[name] = attr
145 143
146 144
147 145 def has_superadmin_permission(apiuser):
148 146 """
149 147 Return True if apiuser is admin or return False
150 148
151 149 :param apiuser:
152 150 """
153 151 if HasPermissionAnyApi('hg.admin')(user=apiuser):
154 152 return True
155 153 return False
156 154
157 155
158 156 def has_repo_permissions(apiuser, repoid, repo, perms):
159 157 """
160 158 Raise JsonRPCError if apiuser is not authorized or return True
161 159
162 160 :param apiuser:
163 161 :param repoid:
164 162 :param repo:
165 163 :param perms:
166 164 """
167 165 if not HasRepoPermissionAnyApi(*perms)(
168 166 user=apiuser, repo_name=repo.repo_name):
169 167 raise JSONRPCError(
170 168 'repository `%s` does not exist' % repoid)
171 169
172 170 return True
173 171
174 172
175 173 def get_user_or_error(userid):
176 174 """
177 175 Get user by id or name or return JsonRPCError if not found
178 176
179 177 :param userid:
180 178 """
181 179 from rhodecode.model.user import UserModel
182 180
183 181 user_model = UserModel()
184 182 try:
185 183 user = user_model.get_user(int(userid))
186 184 except ValueError:
187 185 user = user_model.get_by_username(userid)
188 186
189 187 if user is None:
190 188 raise JSONRPCError("user `%s` does not exist" % (userid,))
191 189 return user
192 190
193 191
194 192 def get_repo_or_error(repoid):
195 193 """
196 194 Get repo by id or name or return JsonRPCError if not found
197 195
198 196 :param repoid:
199 197 """
200 198 from rhodecode.model.repo import RepoModel
201 199
202 200 repo = RepoModel().get_repo(repoid)
203 201 if repo is None:
204 202 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
205 203 return repo
206 204
207 205
208 206 def get_repo_group_or_error(repogroupid):
209 207 """
210 208 Get repo group by id or name or return JsonRPCError if not found
211 209
212 210 :param repogroupid:
213 211 """
214 212 from rhodecode.model.repo_group import RepoGroupModel
215 213
216 214 repo_group = RepoGroupModel()._get_repo_group(repogroupid)
217 215 if repo_group is None:
218 216 raise JSONRPCError(
219 217 'repository group `%s` does not exist' % (repogroupid,))
220 218 return repo_group
221 219
222 220
223 221 def get_user_group_or_error(usergroupid):
224 222 """
225 223 Get user group by id or name or return JsonRPCError if not found
226 224
227 225 :param usergroupid:
228 226 """
229 227 from rhodecode.model.user_group import UserGroupModel
230 228
231 229 user_group = UserGroupModel().get_group(usergroupid)
232 230 if user_group is None:
233 231 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
234 232 return user_group
235 233
236 234
237 235 def get_perm_or_error(permid, prefix=None):
238 236 """
239 237 Get permission by id or name or return JsonRPCError if not found
240 238
241 239 :param permid:
242 240 """
243 241 from rhodecode.model.permission import PermissionModel
244 242
245 243 perm = PermissionModel.cls.get_by_key(permid)
246 244 if perm is None:
247 245 raise JSONRPCError('permission `%s` does not exist' % (permid,))
248 246 if prefix:
249 247 if not perm.permission_name.startswith(prefix):
250 248 raise JSONRPCError('permission `%s` is invalid, '
251 249 'should start with %s' % (permid, prefix))
252 250 return perm
253 251
254 252
255 253 def get_gist_or_error(gistid):
256 254 """
257 255 Get gist by id or gist_access_id or return JsonRPCError if not found
258 256
259 257 :param gistid:
260 258 """
261 259 from rhodecode.model.gist import GistModel
262 260
263 261 gist = GistModel.cls.get_by_access_id(gistid)
264 262 if gist is None:
265 263 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
266 264 return gist
267 265
268 266
269 267 def get_pull_request_or_error(pullrequestid):
270 268 """
271 269 Get pull request by id or return JsonRPCError if not found
272 270
273 271 :param pullrequestid:
274 272 """
275 273 from rhodecode.model.pull_request import PullRequestModel
276 274
277 275 try:
278 276 pull_request = PullRequestModel().get(int(pullrequestid))
279 277 except ValueError:
280 278 raise JSONRPCError('pullrequestid must be an integer')
281 279 if not pull_request:
282 280 raise JSONRPCError('pull request `%s` does not exist' % (
283 281 pullrequestid,))
284 282 return pull_request
285 283
286 284
287 285 def build_commit_data(commit, detail_level):
288 286 parsed_diff = []
289 287 if detail_level == 'extended':
290 288 for f in commit.added:
291 289 parsed_diff.append(_get_commit_dict(filename=f.path, op='A'))
292 290 for f in commit.changed:
293 291 parsed_diff.append(_get_commit_dict(filename=f.path, op='M'))
294 292 for f in commit.removed:
295 293 parsed_diff.append(_get_commit_dict(filename=f.path, op='D'))
296 294
297 295 elif detail_level == 'full':
298 296 from rhodecode.lib.diffs import DiffProcessor
299 297 diff_processor = DiffProcessor(commit.diff())
300 298 for dp in diff_processor.prepare():
301 299 del dp['stats']['ops']
302 300 _stats = dp['stats']
303 301 parsed_diff.append(_get_commit_dict(
304 302 filename=dp['filename'], op=dp['operation'],
305 303 new_revision=dp['new_revision'],
306 304 old_revision=dp['old_revision'],
307 305 raw_diff=dp['raw_diff'], stats=_stats))
308 306
309 307 return parsed_diff
310 308
311 309
312 310 def get_commit_or_error(ref, repo):
313 311 try:
314 312 ref_type, _, ref_hash = ref.split(':')
315 313 except ValueError:
316 314 raise JSONRPCError(
317 315 'Ref `{ref}` given in a wrong format. Please check the API'
318 316 ' documentation for more details'.format(ref=ref))
319 317 try:
320 318 # TODO: dan: refactor this to use repo.scm_instance().get_commit()
321 319 # once get_commit supports ref_types
322 320 return get_commit_from_ref_name(repo, ref_hash)
323 321 except RepositoryError:
324 322 raise JSONRPCError('Ref `{ref}` does not exist'.format(ref=ref))
325 323
326 324
327 325 def resolve_ref_or_error(ref, repo):
328 326 def _parse_ref(type_, name, hash_=None):
329 327 return type_, name, hash_
330 328
331 329 try:
332 330 ref_type, ref_name, ref_hash = _parse_ref(*ref.split(':'))
333 331 except TypeError:
334 332 raise JSONRPCError(
335 333 'Ref `{ref}` given in a wrong format. Please check the API'
336 334 ' documentation for more details'.format(ref=ref))
337 335
338 336 try:
339 337 ref_hash = ref_hash or _get_ref_hash(repo, ref_type, ref_name)
340 338 except (KeyError, ValueError):
341 339 raise JSONRPCError(
342 340 'The specified {type} `{name}` does not exist'.format(
343 341 type=ref_type, name=ref_name))
344 342
345 343 return ':'.join([ref_type, ref_name, ref_hash])
346 344
347 345
348 346 def _get_commit_dict(
349 347 filename, op, new_revision=None, old_revision=None,
350 348 raw_diff=None, stats=None):
351 349 if stats is None:
352 350 stats = {
353 351 "added": None,
354 352 "binary": None,
355 353 "deleted": None
356 354 }
357 355 return {
358 356 "filename": safe_unicode(filename),
359 357 "op": op,
360 358
361 359 # extra details
362 360 "new_revision": new_revision,
363 361 "old_revision": old_revision,
364 362
365 363 "raw_diff": raw_diff,
366 364 "stats": stats
367 365 }
368 366
369 367
370 368 # TODO: mikhail: Think about moving this function to some library
371 369 def _get_ref_hash(repo, type_, name):
372 370 vcs_repo = repo.scm_instance()
373 371 if type_ == 'branch' and vcs_repo.alias in ('hg', 'git'):
374 372 return vcs_repo.branches[name]
375 373 elif type_ == 'bookmark' and vcs_repo.alias == 'hg':
376 374 return vcs_repo.bookmarks[name]
377 375 else:
378 376 raise ValueError()
@@ -1,226 +1,255 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import logging
23 23 import time
24 24
25 25 from rhodecode.api import jsonrpc_method, JSONRPCError
26 from rhodecode.api.exc import JSONRPCValidationError
26 27 from rhodecode.api.utils import (
27 28 Optional, OAttr, get_gist_or_error, get_user_or_error,
28 29 has_superadmin_permission)
29 30 from rhodecode.model.db import Session, or_
30 31 from rhodecode.model.gist import Gist, GistModel
31 32
32 33 log = logging.getLogger(__name__)
33 34
34 35
35 36 @jsonrpc_method()
36 37 def get_gist(request, apiuser, gistid, content=Optional(False)):
37 38 """
38 39 Get the specified gist, based on the gist ID.
39 40
40 41 :param apiuser: This is filled automatically from the |authtoken|.
41 42 :type apiuser: AuthUser
42 43 :param gistid: Set the id of the private or public gist
43 44 :type gistid: str
44 45 :param content: Return the gist content. Default is false.
45 46 :type content: Optional(bool)
46 47 """
47 48
48 49 gist = get_gist_or_error(gistid)
49 50 content = Optional.extract(content)
50 51 if not has_superadmin_permission(apiuser):
51 52 if gist.gist_owner != apiuser.user_id:
52 53 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
53 54 data = gist.get_api_data()
54 55 if content:
55 56 from rhodecode.model.gist import GistModel
56 57 rev, gist_files = GistModel().get_gist_files(gistid)
57 58 data['content'] = dict([(x.path, x.content) for x in gist_files])
58 59 return data
59 60
60 61
61 62 @jsonrpc_method()
62 63 def get_gists(request, apiuser, userid=Optional(OAttr('apiuser'))):
63 64 """
64 65 Get all gists for given user. If userid is empty returned gists
65 66 are for user who called the api
66 67
67 68 :param apiuser: This is filled automatically from the |authtoken|.
68 69 :type apiuser: AuthUser
69 70 :param userid: user to get gists for
70 71 :type userid: Optional(str or int)
71 72 """
72 73
73 74 if not has_superadmin_permission(apiuser):
74 75 # make sure normal user does not pass someone else userid,
75 76 # he is not allowed to do that
76 77 if not isinstance(userid, Optional) and userid != apiuser.user_id:
77 78 raise JSONRPCError(
78 79 'userid is not the same as your user'
79 80 )
80 81
81 82 if isinstance(userid, Optional):
82 83 user_id = apiuser.user_id
83 84 else:
84 85 user_id = get_user_or_error(userid).user_id
85 86
86 87 gists = []
87 88 _gists = Gist().query() \
88 89 .filter(or_(
89 90 Gist.gist_expires == -1, Gist.gist_expires >= time.time())) \
90 91 .filter(Gist.gist_owner == user_id) \
91 92 .order_by(Gist.created_on.desc())
92 93 for gist in _gists:
93 94 gists.append(gist.get_api_data())
94 95 return gists
95 96
96 97
97 98 @jsonrpc_method()
98 99 def create_gist(
99 request, apiuser, files, owner=Optional(OAttr('apiuser')),
100 request, apiuser, files, gistid=Optional(None),
101 owner=Optional(OAttr('apiuser')),
100 102 gist_type=Optional(Gist.GIST_PUBLIC), lifetime=Optional(-1),
101 103 acl_level=Optional(Gist.ACL_LEVEL_PUBLIC),
102 104 description=Optional('')):
103 105 """
104 106 Creates a new Gist.
105 107
106 108 :param apiuser: This is filled automatically from the |authtoken|.
107 109 :type apiuser: AuthUser
108 110 :param files: files to be added to the gist. The data structure has
109 111 to match the following example::
110 112
111 {'filename': {'content':'...', 'lexer': null},
112 'filename2': {'content':'...', 'lexer': null}}
113 {'filename1': {'content':'...'}, 'filename2': {'content':'...'}}
113 114
114 115 :type files: dict
116 :param gistid: Set a custom id for the gist
117 :type gistid: Optional(str)
115 118 :param owner: Set the gist owner, defaults to api method caller
116 119 :type owner: Optional(str or int)
117 120 :param gist_type: type of gist ``public`` or ``private``
118 121 :type gist_type: Optional(str)
119 122 :param lifetime: time in minutes of gist lifetime
120 123 :type lifetime: Optional(int)
121 124 :param acl_level: acl level for this gist, can be
122 125 ``acl_public`` or ``acl_private`` If the value is set to
123 126 ``acl_private`` only logged in users are able to access this gist.
124 127 If not set it defaults to ``acl_public``.
125 128 :type acl_level: Optional(str)
126 129 :param description: gist description
127 130 :type description: Optional(str)
128 131
129 132 Example output:
130 133
131 134 .. code-block:: bash
132 135
133 136 id : <id_given_in_input>
134 137 result : {
135 138 "msg": "created new gist",
136 139 "gist": {}
137 140 }
138 141 error : null
139 142
140 143 Example error output:
141 144
142 145 .. code-block:: bash
143 146
144 147 id : <id_given_in_input>
145 148 result : null
146 149 error : {
147 150 "failed to create gist"
148 151 }
149 152
150 153 """
154 from rhodecode.model import validation_schema
155 from rhodecode.model.validation_schema.schemas import gist_schema
151 156
152 try:
153 157 if isinstance(owner, Optional):
154 158 owner = apiuser.user_id
155 159
156 160 owner = get_user_or_error(owner)
157 description = Optional.extract(description)
158 gist_type = Optional.extract(gist_type)
161
159 162 lifetime = Optional.extract(lifetime)
160 acl_level = Optional.extract(acl_level)
163 schema = gist_schema.GistSchema().bind(
164 # bind the given values if it's allowed, however the deferred
165 # validator will still validate it according to other rules
166 lifetime_options=[lifetime])
167
168 try:
169 nodes = gist_schema.nodes_to_sequence(
170 files, colander_node=schema.get('nodes'))
161 171
162 gist = GistModel().create(description=description,
172 schema_data = schema.deserialize(dict(
173 gistid=Optional.extract(gistid),
174 description=Optional.extract(description),
175 gist_type=Optional.extract(gist_type),
176 lifetime=lifetime,
177 gist_acl_level=Optional.extract(acl_level),
178 nodes=nodes
179 ))
180
181 # convert to safer format with just KEYs so we sure no duplicates
182 schema_data['nodes'] = gist_schema.sequence_to_nodes(
183 schema_data['nodes'], colander_node=schema.get('nodes'))
184
185 except validation_schema.Invalid as err:
186 raise JSONRPCValidationError(colander_exc=err)
187
188 try:
189 gist = GistModel().create(
163 190 owner=owner,
164 gist_mapping=files,
165 gist_type=gist_type,
166 lifetime=lifetime,
167 gist_acl_level=acl_level)
191 gist_id=schema_data['gistid'],
192 description=schema_data['description'],
193 gist_mapping=schema_data['nodes'],
194 gist_type=schema_data['gist_type'],
195 lifetime=schema_data['lifetime'],
196 gist_acl_level=schema_data['gist_acl_level'])
168 197 Session().commit()
169 198 return {
170 199 'msg': 'created new gist',
171 200 'gist': gist.get_api_data()
172 201 }
173 202 except Exception:
174 203 log.exception('Error occurred during creation of gist')
175 204 raise JSONRPCError('failed to create gist')
176 205
177 206
178 207 @jsonrpc_method()
179 208 def delete_gist(request, apiuser, gistid):
180 209 """
181 210 Deletes existing gist
182 211
183 212 :param apiuser: filled automatically from apikey
184 213 :type apiuser: AuthUser
185 214 :param gistid: id of gist to delete
186 215 :type gistid: str
187 216
188 217 Example output:
189 218
190 219 .. code-block:: bash
191 220
192 221 id : <id_given_in_input>
193 222 result : {
194 223 "deleted gist ID: <gist_id>",
195 224 "gist": null
196 225 }
197 226 error : null
198 227
199 228 Example error output:
200 229
201 230 .. code-block:: bash
202 231
203 232 id : <id_given_in_input>
204 233 result : null
205 234 error : {
206 235 "failed to delete gist ID:<gist_id>"
207 236 }
208 237
209 238 """
210 239
211 240 gist = get_gist_or_error(gistid)
212 241 if not has_superadmin_permission(apiuser):
213 242 if gist.gist_owner != apiuser.user_id:
214 243 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
215 244
216 245 try:
217 246 GistModel().delete(gist)
218 247 Session().commit()
219 248 return {
220 249 'msg': 'deleted gist ID:%s' % (gist.gist_access_id,),
221 250 'gist': None
222 251 }
223 252 except Exception:
224 253 log.exception('Error occured during gist deletion')
225 254 raise JSONRPCError('failed to delete gist ID:%s'
226 255 % (gist.gist_access_id,)) No newline at end of file
@@ -1,1886 +1,1886 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2016 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 colander
25 25
26 26 from rhodecode import BACKENDS
27 27 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCForbidden, json
28 28 from rhodecode.api.utils import (
29 29 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
30 30 get_user_group_or_error, get_user_or_error, has_repo_permissions,
31 31 get_perm_or_error, store_update, get_repo_group_or_error, parse_args,
32 32 get_origin, build_commit_data)
33 33 from rhodecode.lib.auth import (
34 34 HasPermissionAnyApi, HasRepoGroupPermissionAnyApi,
35 35 HasUserGroupPermissionAnyApi)
36 36 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
37 37 from rhodecode.lib.utils import map_groups
38 38 from rhodecode.lib.utils2 import str2bool, time_to_datetime
39 39 from rhodecode.model.changeset_status import ChangesetStatusModel
40 40 from rhodecode.model.comment import ChangesetCommentsModel
41 41 from rhodecode.model.db import (
42 42 Session, ChangesetStatus, RepositoryField, Repository)
43 43 from rhodecode.model.repo import RepoModel
44 44 from rhodecode.model.repo_group import RepoGroupModel
45 45 from rhodecode.model.scm import ScmModel, RepoList
46 46 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
47 from rhodecode.model.validation_schema import RepoSchema
47 from rhodecode.model.validation_schema.schemas import repo_schema
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51
52 52 @jsonrpc_method()
53 53 def get_repo(request, apiuser, repoid, cache=Optional(True)):
54 54 """
55 55 Gets an existing repository by its name or repository_id.
56 56
57 57 The members section so the output returns users groups or users
58 58 associated with that repository.
59 59
60 60 This command can only be run using an |authtoken| with admin rights,
61 61 or users with at least read rights to the |repo|.
62 62
63 63 :param apiuser: This is filled automatically from the |authtoken|.
64 64 :type apiuser: AuthUser
65 65 :param repoid: The repository name or repository id.
66 66 :type repoid: str or int
67 67 :param cache: use the cached value for last changeset
68 68 :type: cache: Optional(bool)
69 69
70 70 Example output:
71 71
72 72 .. code-block:: bash
73 73
74 74 {
75 75 "error": null,
76 76 "id": <repo_id>,
77 77 "result": {
78 78 "clone_uri": null,
79 79 "created_on": "timestamp",
80 80 "description": "repo description",
81 81 "enable_downloads": false,
82 82 "enable_locking": false,
83 83 "enable_statistics": false,
84 84 "followers": [
85 85 {
86 86 "active": true,
87 87 "admin": false,
88 88 "api_key": "****************************************",
89 89 "api_keys": [
90 90 "****************************************"
91 91 ],
92 92 "email": "user@example.com",
93 93 "emails": [
94 94 "user@example.com"
95 95 ],
96 96 "extern_name": "rhodecode",
97 97 "extern_type": "rhodecode",
98 98 "firstname": "username",
99 99 "ip_addresses": [],
100 100 "language": null,
101 101 "last_login": "2015-09-16T17:16:35.854",
102 102 "lastname": "surname",
103 103 "user_id": <user_id>,
104 104 "username": "name"
105 105 }
106 106 ],
107 107 "fork_of": "parent-repo",
108 108 "landing_rev": [
109 109 "rev",
110 110 "tip"
111 111 ],
112 112 "last_changeset": {
113 113 "author": "User <user@example.com>",
114 114 "branch": "default",
115 115 "date": "timestamp",
116 116 "message": "last commit message",
117 117 "parents": [
118 118 {
119 119 "raw_id": "commit-id"
120 120 }
121 121 ],
122 122 "raw_id": "commit-id",
123 123 "revision": <revision number>,
124 124 "short_id": "short id"
125 125 },
126 126 "lock_reason": null,
127 127 "locked_by": null,
128 128 "locked_date": null,
129 129 "members": [
130 130 {
131 131 "name": "super-admin-name",
132 132 "origin": "super-admin",
133 133 "permission": "repository.admin",
134 134 "type": "user"
135 135 },
136 136 {
137 137 "name": "owner-name",
138 138 "origin": "owner",
139 139 "permission": "repository.admin",
140 140 "type": "user"
141 141 },
142 142 {
143 143 "name": "user-group-name",
144 144 "origin": "permission",
145 145 "permission": "repository.write",
146 146 "type": "user_group"
147 147 }
148 148 ],
149 149 "owner": "owner-name",
150 150 "permissions": [
151 151 {
152 152 "name": "super-admin-name",
153 153 "origin": "super-admin",
154 154 "permission": "repository.admin",
155 155 "type": "user"
156 156 },
157 157 {
158 158 "name": "owner-name",
159 159 "origin": "owner",
160 160 "permission": "repository.admin",
161 161 "type": "user"
162 162 },
163 163 {
164 164 "name": "user-group-name",
165 165 "origin": "permission",
166 166 "permission": "repository.write",
167 167 "type": "user_group"
168 168 }
169 169 ],
170 170 "private": true,
171 171 "repo_id": 676,
172 172 "repo_name": "user-group/repo-name",
173 173 "repo_type": "hg"
174 174 }
175 175 }
176 176 """
177 177
178 178 repo = get_repo_or_error(repoid)
179 179 cache = Optional.extract(cache)
180 180 include_secrets = False
181 181 if has_superadmin_permission(apiuser):
182 182 include_secrets = True
183 183 else:
184 184 # check if we have at least read permission for this repo !
185 185 _perms = (
186 186 'repository.admin', 'repository.write', 'repository.read',)
187 187 has_repo_permissions(apiuser, repoid, repo, _perms)
188 188
189 189 permissions = []
190 190 for _user in repo.permissions():
191 191 user_data = {
192 192 'name': _user.username,
193 193 'permission': _user.permission,
194 194 'origin': get_origin(_user),
195 195 'type': "user",
196 196 }
197 197 permissions.append(user_data)
198 198
199 199 for _user_group in repo.permission_user_groups():
200 200 user_group_data = {
201 201 'name': _user_group.users_group_name,
202 202 'permission': _user_group.permission,
203 203 'origin': get_origin(_user_group),
204 204 'type': "user_group",
205 205 }
206 206 permissions.append(user_group_data)
207 207
208 208 following_users = [
209 209 user.user.get_api_data(include_secrets=include_secrets)
210 210 for user in repo.followers]
211 211
212 212 if not cache:
213 213 repo.update_commit_cache()
214 214 data = repo.get_api_data(include_secrets=include_secrets)
215 215 data['members'] = permissions # TODO: this should be deprecated soon
216 216 data['permissions'] = permissions
217 217 data['followers'] = following_users
218 218 return data
219 219
220 220
221 221 @jsonrpc_method()
222 222 def get_repos(request, apiuser):
223 223 """
224 224 Lists all existing repositories.
225 225
226 226 This command can only be run using an |authtoken| with admin rights,
227 227 or users with at least read rights to |repos|.
228 228
229 229 :param apiuser: This is filled automatically from the |authtoken|.
230 230 :type apiuser: AuthUser
231 231
232 232 Example output:
233 233
234 234 .. code-block:: bash
235 235
236 236 id : <id_given_in_input>
237 237 result: [
238 238 {
239 239 "repo_id" : "<repo_id>",
240 240 "repo_name" : "<reponame>"
241 241 "repo_type" : "<repo_type>",
242 242 "clone_uri" : "<clone_uri>",
243 243 "private": : "<bool>",
244 244 "created_on" : "<datetimecreated>",
245 245 "description" : "<description>",
246 246 "landing_rev": "<landing_rev>",
247 247 "owner": "<repo_owner>",
248 248 "fork_of": "<name_of_fork_parent>",
249 249 "enable_downloads": "<bool>",
250 250 "enable_locking": "<bool>",
251 251 "enable_statistics": "<bool>",
252 252 },
253 253 ...
254 254 ]
255 255 error: null
256 256 """
257 257
258 258 include_secrets = has_superadmin_permission(apiuser)
259 259 _perms = ('repository.read', 'repository.write', 'repository.admin',)
260 260 extras = {'user': apiuser}
261 261
262 262 repo_list = RepoList(
263 263 RepoModel().get_all(), perm_set=_perms, extra_kwargs=extras)
264 264 return [repo.get_api_data(include_secrets=include_secrets)
265 265 for repo in repo_list]
266 266
267 267
268 268 @jsonrpc_method()
269 269 def get_repo_changeset(request, apiuser, repoid, revision,
270 270 details=Optional('basic')):
271 271 """
272 272 Returns information about a changeset.
273 273
274 274 Additionally parameters define the amount of details returned by
275 275 this function.
276 276
277 277 This command can only be run using an |authtoken| with admin rights,
278 278 or users with at least read rights to the |repo|.
279 279
280 280 :param apiuser: This is filled automatically from the |authtoken|.
281 281 :type apiuser: AuthUser
282 282 :param repoid: The repository name or repository id
283 283 :type repoid: str or int
284 284 :param revision: revision for which listing should be done
285 285 :type revision: str
286 286 :param details: details can be 'basic|extended|full' full gives diff
287 287 info details like the diff itself, and number of changed files etc.
288 288 :type details: Optional(str)
289 289
290 290 """
291 291 repo = get_repo_or_error(repoid)
292 292 if not has_superadmin_permission(apiuser):
293 293 _perms = (
294 294 'repository.admin', 'repository.write', 'repository.read',)
295 295 has_repo_permissions(apiuser, repoid, repo, _perms)
296 296
297 297 changes_details = Optional.extract(details)
298 298 _changes_details_types = ['basic', 'extended', 'full']
299 299 if changes_details not in _changes_details_types:
300 300 raise JSONRPCError(
301 301 'ret_type must be one of %s' % (
302 302 ','.join(_changes_details_types)))
303 303
304 304 pre_load = ['author', 'branch', 'date', 'message', 'parents',
305 305 'status', '_commit', '_file_paths']
306 306
307 307 try:
308 308 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
309 309 except TypeError as e:
310 310 raise JSONRPCError(e.message)
311 311 _cs_json = cs.__json__()
312 312 _cs_json['diff'] = build_commit_data(cs, changes_details)
313 313 if changes_details == 'full':
314 314 _cs_json['refs'] = {
315 315 'branches': [cs.branch],
316 316 'bookmarks': getattr(cs, 'bookmarks', []),
317 317 'tags': cs.tags
318 318 }
319 319 return _cs_json
320 320
321 321
322 322 @jsonrpc_method()
323 323 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
324 324 details=Optional('basic')):
325 325 """
326 326 Returns a set of commits limited by the number starting
327 327 from the `start_rev` option.
328 328
329 329 Additional parameters define the amount of details returned by this
330 330 function.
331 331
332 332 This command can only be run using an |authtoken| with admin rights,
333 333 or users with at least read rights to |repos|.
334 334
335 335 :param apiuser: This is filled automatically from the |authtoken|.
336 336 :type apiuser: AuthUser
337 337 :param repoid: The repository name or repository ID.
338 338 :type repoid: str or int
339 339 :param start_rev: The starting revision from where to get changesets.
340 340 :type start_rev: str
341 341 :param limit: Limit the number of commits to this amount
342 342 :type limit: str or int
343 343 :param details: Set the level of detail returned. Valid option are:
344 344 ``basic``, ``extended`` and ``full``.
345 345 :type details: Optional(str)
346 346
347 347 .. note::
348 348
349 349 Setting the parameter `details` to the value ``full`` is extensive
350 350 and returns details like the diff itself, and the number
351 351 of changed files.
352 352
353 353 """
354 354 repo = get_repo_or_error(repoid)
355 355 if not has_superadmin_permission(apiuser):
356 356 _perms = (
357 357 'repository.admin', 'repository.write', 'repository.read',)
358 358 has_repo_permissions(apiuser, repoid, repo, _perms)
359 359
360 360 changes_details = Optional.extract(details)
361 361 _changes_details_types = ['basic', 'extended', 'full']
362 362 if changes_details not in _changes_details_types:
363 363 raise JSONRPCError(
364 364 'ret_type must be one of %s' % (
365 365 ','.join(_changes_details_types)))
366 366
367 367 limit = int(limit)
368 368 pre_load = ['author', 'branch', 'date', 'message', 'parents',
369 369 'status', '_commit', '_file_paths']
370 370
371 371 vcs_repo = repo.scm_instance()
372 372 # SVN needs a special case to distinguish its index and commit id
373 373 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
374 374 start_rev = vcs_repo.commit_ids[0]
375 375
376 376 try:
377 377 commits = vcs_repo.get_commits(
378 378 start_id=start_rev, pre_load=pre_load)
379 379 except TypeError as e:
380 380 raise JSONRPCError(e.message)
381 381 except Exception:
382 382 log.exception('Fetching of commits failed')
383 383 raise JSONRPCError('Error occurred during commit fetching')
384 384
385 385 ret = []
386 386 for cnt, commit in enumerate(commits):
387 387 if cnt >= limit != -1:
388 388 break
389 389 _cs_json = commit.__json__()
390 390 _cs_json['diff'] = build_commit_data(commit, changes_details)
391 391 if changes_details == 'full':
392 392 _cs_json['refs'] = {
393 393 'branches': [commit.branch],
394 394 'bookmarks': getattr(commit, 'bookmarks', []),
395 395 'tags': commit.tags
396 396 }
397 397 ret.append(_cs_json)
398 398 return ret
399 399
400 400
401 401 @jsonrpc_method()
402 402 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
403 403 ret_type=Optional('all'), details=Optional('basic'),
404 404 max_file_bytes=Optional(None)):
405 405 """
406 406 Returns a list of nodes and children in a flat list for a given
407 407 path at given revision.
408 408
409 409 It's possible to specify ret_type to show only `files` or `dirs`.
410 410
411 411 This command can only be run using an |authtoken| with admin rights,
412 412 or users with at least read rights to |repos|.
413 413
414 414 :param apiuser: This is filled automatically from the |authtoken|.
415 415 :type apiuser: AuthUser
416 416 :param repoid: The repository name or repository ID.
417 417 :type repoid: str or int
418 418 :param revision: The revision for which listing should be done.
419 419 :type revision: str
420 420 :param root_path: The path from which to start displaying.
421 421 :type root_path: str
422 422 :param ret_type: Set the return type. Valid options are
423 423 ``all`` (default), ``files`` and ``dirs``.
424 424 :type ret_type: Optional(str)
425 425 :param details: Returns extended information about nodes, such as
426 426 md5, binary, and or content. The valid options are ``basic`` and
427 427 ``full``.
428 428 :type details: Optional(str)
429 429 :param max_file_bytes: Only return file content under this file size bytes
430 430 :type details: Optional(int)
431 431
432 432 Example output:
433 433
434 434 .. code-block:: bash
435 435
436 436 id : <id_given_in_input>
437 437 result: [
438 438 {
439 439 "name" : "<name>"
440 440 "type" : "<type>",
441 441 "binary": "<true|false>" (only in extended mode)
442 442 "md5" : "<md5 of file content>" (only in extended mode)
443 443 },
444 444 ...
445 445 ]
446 446 error: null
447 447 """
448 448
449 449 repo = get_repo_or_error(repoid)
450 450 if not has_superadmin_permission(apiuser):
451 451 _perms = (
452 452 'repository.admin', 'repository.write', 'repository.read',)
453 453 has_repo_permissions(apiuser, repoid, repo, _perms)
454 454
455 455 ret_type = Optional.extract(ret_type)
456 456 details = Optional.extract(details)
457 457 _extended_types = ['basic', 'full']
458 458 if details not in _extended_types:
459 459 raise JSONRPCError(
460 460 'ret_type must be one of %s' % (','.join(_extended_types)))
461 461 extended_info = False
462 462 content = False
463 463 if details == 'basic':
464 464 extended_info = True
465 465
466 466 if details == 'full':
467 467 extended_info = content = True
468 468
469 469 _map = {}
470 470 try:
471 471 # check if repo is not empty by any chance, skip quicker if it is.
472 472 _scm = repo.scm_instance()
473 473 if _scm.is_empty():
474 474 return []
475 475
476 476 _d, _f = ScmModel().get_nodes(
477 477 repo, revision, root_path, flat=False,
478 478 extended_info=extended_info, content=content,
479 479 max_file_bytes=max_file_bytes)
480 480 _map = {
481 481 'all': _d + _f,
482 482 'files': _f,
483 483 'dirs': _d,
484 484 }
485 485 return _map[ret_type]
486 486 except KeyError:
487 487 raise JSONRPCError(
488 488 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
489 489 except Exception:
490 490 log.exception("Exception occurred while trying to get repo nodes")
491 491 raise JSONRPCError(
492 492 'failed to get repo: `%s` nodes' % repo.repo_name
493 493 )
494 494
495 495
496 496 @jsonrpc_method()
497 497 def get_repo_refs(request, apiuser, repoid):
498 498 """
499 499 Returns a dictionary of current references. It returns
500 500 bookmarks, branches, closed_branches, and tags for given repository
501 501
502 502 It's possible to specify ret_type to show only `files` or `dirs`.
503 503
504 504 This command can only be run using an |authtoken| with admin rights,
505 505 or users with at least read rights to |repos|.
506 506
507 507 :param apiuser: This is filled automatically from the |authtoken|.
508 508 :type apiuser: AuthUser
509 509 :param repoid: The repository name or repository ID.
510 510 :type repoid: str or int
511 511
512 512 Example output:
513 513
514 514 .. code-block:: bash
515 515
516 516 id : <id_given_in_input>
517 517 result: [
518 518 TODO...
519 519 ]
520 520 error: null
521 521 """
522 522
523 523 repo = get_repo_or_error(repoid)
524 524 if not has_superadmin_permission(apiuser):
525 525 _perms = ('repository.admin', 'repository.write', 'repository.read',)
526 526 has_repo_permissions(apiuser, repoid, repo, _perms)
527 527
528 528 try:
529 529 # check if repo is not empty by any chance, skip quicker if it is.
530 530 vcs_instance = repo.scm_instance()
531 531 refs = vcs_instance.refs()
532 532 return refs
533 533 except Exception:
534 534 log.exception("Exception occurred while trying to get repo refs")
535 535 raise JSONRPCError(
536 536 'failed to get repo: `%s` references' % repo.repo_name
537 537 )
538 538
539 539
540 540 @jsonrpc_method()
541 541 def create_repo(request, apiuser, repo_name, repo_type,
542 542 owner=Optional(OAttr('apiuser')), description=Optional(''),
543 543 private=Optional(False), clone_uri=Optional(None),
544 544 landing_rev=Optional('rev:tip'),
545 545 enable_statistics=Optional(False),
546 546 enable_locking=Optional(False),
547 547 enable_downloads=Optional(False),
548 548 copy_permissions=Optional(False)):
549 549 """
550 550 Creates a repository.
551 551
552 552 * If the repository name contains "/", all the required repository
553 553 groups will be created.
554 554
555 555 For example "foo/bar/baz" will create |repo| groups "foo" and "bar"
556 556 (with "foo" as parent). It will also create the "baz" repository
557 557 with "bar" as |repo| group.
558 558
559 559 This command can only be run using an |authtoken| with at least
560 560 write permissions to the |repo|.
561 561
562 562 :param apiuser: This is filled automatically from the |authtoken|.
563 563 :type apiuser: AuthUser
564 564 :param repo_name: Set the repository name.
565 565 :type repo_name: str
566 566 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
567 567 :type repo_type: str
568 568 :param owner: user_id or username
569 569 :type owner: Optional(str)
570 570 :param description: Set the repository description.
571 571 :type description: Optional(str)
572 572 :param private:
573 573 :type private: bool
574 574 :param clone_uri:
575 575 :type clone_uri: str
576 576 :param landing_rev: <rev_type>:<rev>
577 577 :type landing_rev: str
578 578 :param enable_locking:
579 579 :type enable_locking: bool
580 580 :param enable_downloads:
581 581 :type enable_downloads: bool
582 582 :param enable_statistics:
583 583 :type enable_statistics: bool
584 584 :param copy_permissions: Copy permission from group in which the
585 585 repository is being created.
586 586 :type copy_permissions: bool
587 587
588 588
589 589 Example output:
590 590
591 591 .. code-block:: bash
592 592
593 593 id : <id_given_in_input>
594 594 result: {
595 595 "msg": "Created new repository `<reponame>`",
596 596 "success": true,
597 597 "task": "<celery task id or None if done sync>"
598 598 }
599 599 error: null
600 600
601 601
602 602 Example error output:
603 603
604 604 .. code-block:: bash
605 605
606 606 id : <id_given_in_input>
607 607 result : null
608 608 error : {
609 609 'failed to create repository `<repo_name>`
610 610 }
611 611
612 612 """
613 schema = RepoSchema()
613 schema = repo_schema.RepoSchema()
614 614 try:
615 615 data = schema.deserialize({
616 616 'repo_name': repo_name
617 617 })
618 618 except colander.Invalid as e:
619 619 raise JSONRPCError("Validation failed: %s" % (e.asdict(),))
620 620 repo_name = data['repo_name']
621 621
622 622 (repo_name_cleaned,
623 623 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(
624 624 repo_name)
625 625
626 626 if not HasPermissionAnyApi(
627 627 'hg.admin', 'hg.create.repository')(user=apiuser):
628 628 # check if we have admin permission for this repo group if given !
629 629
630 630 if parent_group_name:
631 631 repogroupid = parent_group_name
632 632 repo_group = get_repo_group_or_error(parent_group_name)
633 633
634 634 _perms = ('group.admin',)
635 635 if not HasRepoGroupPermissionAnyApi(*_perms)(
636 636 user=apiuser, group_name=repo_group.group_name):
637 637 raise JSONRPCError(
638 638 'repository group `%s` does not exist' % (
639 639 repogroupid,))
640 640 else:
641 641 raise JSONRPCForbidden()
642 642
643 643 if not has_superadmin_permission(apiuser):
644 644 if not isinstance(owner, Optional):
645 645 # forbid setting owner for non-admins
646 646 raise JSONRPCError(
647 647 'Only RhodeCode admin can specify `owner` param')
648 648
649 649 if isinstance(owner, Optional):
650 650 owner = apiuser.user_id
651 651
652 652 owner = get_user_or_error(owner)
653 653
654 654 if RepoModel().get_by_repo_name(repo_name):
655 655 raise JSONRPCError("repo `%s` already exist" % repo_name)
656 656
657 657 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
658 658 if isinstance(private, Optional):
659 659 private = defs.get('repo_private') or Optional.extract(private)
660 660 if isinstance(repo_type, Optional):
661 661 repo_type = defs.get('repo_type')
662 662 if isinstance(enable_statistics, Optional):
663 663 enable_statistics = defs.get('repo_enable_statistics')
664 664 if isinstance(enable_locking, Optional):
665 665 enable_locking = defs.get('repo_enable_locking')
666 666 if isinstance(enable_downloads, Optional):
667 667 enable_downloads = defs.get('repo_enable_downloads')
668 668
669 669 clone_uri = Optional.extract(clone_uri)
670 670 description = Optional.extract(description)
671 671 landing_rev = Optional.extract(landing_rev)
672 672 copy_permissions = Optional.extract(copy_permissions)
673 673
674 674 try:
675 675 # create structure of groups and return the last group
676 676 repo_group = map_groups(repo_name)
677 677 data = {
678 678 'repo_name': repo_name_cleaned,
679 679 'repo_name_full': repo_name,
680 680 'repo_type': repo_type,
681 681 'repo_description': description,
682 682 'owner': owner,
683 683 'repo_private': private,
684 684 'clone_uri': clone_uri,
685 685 'repo_group': repo_group.group_id if repo_group else None,
686 686 'repo_landing_rev': landing_rev,
687 687 'enable_statistics': enable_statistics,
688 688 'enable_locking': enable_locking,
689 689 'enable_downloads': enable_downloads,
690 690 'repo_copy_permissions': copy_permissions,
691 691 }
692 692
693 693 if repo_type not in BACKENDS.keys():
694 694 raise Exception("Invalid backend type %s" % repo_type)
695 695 task = RepoModel().create(form_data=data, cur_user=owner)
696 696 from celery.result import BaseAsyncResult
697 697 task_id = None
698 698 if isinstance(task, BaseAsyncResult):
699 699 task_id = task.task_id
700 700 # no commit, it's done in RepoModel, or async via celery
701 701 return {
702 702 'msg': "Created new repository `%s`" % (repo_name,),
703 703 'success': True, # cannot return the repo data here since fork
704 704 # cann be done async
705 705 'task': task_id
706 706 }
707 707 except Exception:
708 708 log.exception(
709 709 u"Exception while trying to create the repository %s",
710 710 repo_name)
711 711 raise JSONRPCError(
712 712 'failed to create repository `%s`' % (repo_name,))
713 713
714 714
715 715 @jsonrpc_method()
716 716 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
717 717 description=Optional('')):
718 718 """
719 719 Adds an extra field to a repository.
720 720
721 721 This command can only be run using an |authtoken| with at least
722 722 write permissions to the |repo|.
723 723
724 724 :param apiuser: This is filled automatically from the |authtoken|.
725 725 :type apiuser: AuthUser
726 726 :param repoid: Set the repository name or repository id.
727 727 :type repoid: str or int
728 728 :param key: Create a unique field key for this repository.
729 729 :type key: str
730 730 :param label:
731 731 :type label: Optional(str)
732 732 :param description:
733 733 :type description: Optional(str)
734 734 """
735 735 repo = get_repo_or_error(repoid)
736 736 if not has_superadmin_permission(apiuser):
737 737 _perms = ('repository.admin',)
738 738 has_repo_permissions(apiuser, repoid, repo, _perms)
739 739
740 740 label = Optional.extract(label) or key
741 741 description = Optional.extract(description)
742 742
743 743 field = RepositoryField.get_by_key_name(key, repo)
744 744 if field:
745 745 raise JSONRPCError('Field with key '
746 746 '`%s` exists for repo `%s`' % (key, repoid))
747 747
748 748 try:
749 749 RepoModel().add_repo_field(repo, key, field_label=label,
750 750 field_desc=description)
751 751 Session().commit()
752 752 return {
753 753 'msg': "Added new repository field `%s`" % (key,),
754 754 'success': True,
755 755 }
756 756 except Exception:
757 757 log.exception("Exception occurred while trying to add field to repo")
758 758 raise JSONRPCError(
759 759 'failed to create new field for repository `%s`' % (repoid,))
760 760
761 761
762 762 @jsonrpc_method()
763 763 def remove_field_from_repo(request, apiuser, repoid, key):
764 764 """
765 765 Removes an extra field from a repository.
766 766
767 767 This command can only be run using an |authtoken| with at least
768 768 write permissions to the |repo|.
769 769
770 770 :param apiuser: This is filled automatically from the |authtoken|.
771 771 :type apiuser: AuthUser
772 772 :param repoid: Set the repository name or repository ID.
773 773 :type repoid: str or int
774 774 :param key: Set the unique field key for this repository.
775 775 :type key: str
776 776 """
777 777
778 778 repo = get_repo_or_error(repoid)
779 779 if not has_superadmin_permission(apiuser):
780 780 _perms = ('repository.admin',)
781 781 has_repo_permissions(apiuser, repoid, repo, _perms)
782 782
783 783 field = RepositoryField.get_by_key_name(key, repo)
784 784 if not field:
785 785 raise JSONRPCError('Field with key `%s` does not '
786 786 'exists for repo `%s`' % (key, repoid))
787 787
788 788 try:
789 789 RepoModel().delete_repo_field(repo, field_key=key)
790 790 Session().commit()
791 791 return {
792 792 'msg': "Deleted repository field `%s`" % (key,),
793 793 'success': True,
794 794 }
795 795 except Exception:
796 796 log.exception(
797 797 "Exception occurred while trying to delete field from repo")
798 798 raise JSONRPCError(
799 799 'failed to delete field for repository `%s`' % (repoid,))
800 800
801 801
802 802 @jsonrpc_method()
803 803 def update_repo(request, apiuser, repoid, name=Optional(None),
804 804 owner=Optional(OAttr('apiuser')),
805 805 group=Optional(None),
806 806 fork_of=Optional(None),
807 807 description=Optional(''), private=Optional(False),
808 808 clone_uri=Optional(None), landing_rev=Optional('rev:tip'),
809 809 enable_statistics=Optional(False),
810 810 enable_locking=Optional(False),
811 811 enable_downloads=Optional(False),
812 812 fields=Optional('')):
813 813 """
814 814 Updates a repository with the given information.
815 815
816 816 This command can only be run using an |authtoken| with at least
817 817 write permissions to the |repo|.
818 818
819 819 :param apiuser: This is filled automatically from the |authtoken|.
820 820 :type apiuser: AuthUser
821 821 :param repoid: repository name or repository ID.
822 822 :type repoid: str or int
823 823 :param name: Update the |repo| name.
824 824 :type name: str
825 825 :param owner: Set the |repo| owner.
826 826 :type owner: str
827 827 :param group: Set the |repo| group the |repo| belongs to.
828 828 :type group: str
829 829 :param fork_of: Set the master |repo| name.
830 830 :type fork_of: str
831 831 :param description: Update the |repo| description.
832 832 :type description: str
833 833 :param private: Set the |repo| as private. (True | False)
834 834 :type private: bool
835 835 :param clone_uri: Update the |repo| clone URI.
836 836 :type clone_uri: str
837 837 :param landing_rev: Set the |repo| landing revision. Default is
838 838 ``tip``.
839 839 :type landing_rev: str
840 840 :param enable_statistics: Enable statistics on the |repo|,
841 841 (True | False).
842 842 :type enable_statistics: bool
843 843 :param enable_locking: Enable |repo| locking.
844 844 :type enable_locking: bool
845 845 :param enable_downloads: Enable downloads from the |repo|,
846 846 (True | False).
847 847 :type enable_downloads: bool
848 848 :param fields: Add extra fields to the |repo|. Use the following
849 849 example format: ``field_key=field_val,field_key2=fieldval2``.
850 850 Escape ', ' with \,
851 851 :type fields: str
852 852 """
853 853 repo = get_repo_or_error(repoid)
854 854 include_secrets = False
855 855 if has_superadmin_permission(apiuser):
856 856 include_secrets = True
857 857 else:
858 858 _perms = ('repository.admin',)
859 859 has_repo_permissions(apiuser, repoid, repo, _perms)
860 860
861 861 updates = {
862 862 # update function requires this.
863 863 'repo_name': repo.just_name
864 864 }
865 865 repo_group = group
866 866 if not isinstance(repo_group, Optional):
867 867 repo_group = get_repo_group_or_error(repo_group)
868 868 repo_group = repo_group.group_id
869 869
870 870 repo_fork_of = fork_of
871 871 if not isinstance(repo_fork_of, Optional):
872 872 repo_fork_of = get_repo_or_error(repo_fork_of)
873 873 repo_fork_of = repo_fork_of.repo_id
874 874
875 875 try:
876 876 store_update(updates, name, 'repo_name')
877 877 store_update(updates, repo_group, 'repo_group')
878 878 store_update(updates, repo_fork_of, 'fork_id')
879 879 store_update(updates, owner, 'user')
880 880 store_update(updates, description, 'repo_description')
881 881 store_update(updates, private, 'repo_private')
882 882 store_update(updates, clone_uri, 'clone_uri')
883 883 store_update(updates, landing_rev, 'repo_landing_rev')
884 884 store_update(updates, enable_statistics, 'repo_enable_statistics')
885 885 store_update(updates, enable_locking, 'repo_enable_locking')
886 886 store_update(updates, enable_downloads, 'repo_enable_downloads')
887 887
888 888 # extra fields
889 889 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
890 890 if fields:
891 891 updates.update(fields)
892 892
893 893 RepoModel().update(repo, **updates)
894 894 Session().commit()
895 895 return {
896 896 'msg': 'updated repo ID:%s %s' % (
897 897 repo.repo_id, repo.repo_name),
898 898 'repository': repo.get_api_data(
899 899 include_secrets=include_secrets)
900 900 }
901 901 except Exception:
902 902 log.exception(
903 903 u"Exception while trying to update the repository %s",
904 904 repoid)
905 905 raise JSONRPCError('failed to update repo `%s`' % repoid)
906 906
907 907
908 908 @jsonrpc_method()
909 909 def fork_repo(request, apiuser, repoid, fork_name,
910 910 owner=Optional(OAttr('apiuser')),
911 911 description=Optional(''), copy_permissions=Optional(False),
912 912 private=Optional(False), landing_rev=Optional('rev:tip')):
913 913 """
914 914 Creates a fork of the specified |repo|.
915 915
916 916 * If using |RCE| with Celery this will immediately return a success
917 917 message, even though the fork will be created asynchronously.
918 918
919 919 This command can only be run using an |authtoken| with fork
920 920 permissions on the |repo|.
921 921
922 922 :param apiuser: This is filled automatically from the |authtoken|.
923 923 :type apiuser: AuthUser
924 924 :param repoid: Set repository name or repository ID.
925 925 :type repoid: str or int
926 926 :param fork_name: Set the fork name.
927 927 :type fork_name: str
928 928 :param owner: Set the fork owner.
929 929 :type owner: str
930 930 :param description: Set the fork descripton.
931 931 :type description: str
932 932 :param copy_permissions: Copy permissions from parent |repo|. The
933 933 default is False.
934 934 :type copy_permissions: bool
935 935 :param private: Make the fork private. The default is False.
936 936 :type private: bool
937 937 :param landing_rev: Set the landing revision. The default is tip.
938 938
939 939 Example output:
940 940
941 941 .. code-block:: bash
942 942
943 943 id : <id_for_response>
944 944 api_key : "<api_key>"
945 945 args: {
946 946 "repoid" : "<reponame or repo_id>",
947 947 "fork_name": "<forkname>",
948 948 "owner": "<username or user_id = Optional(=apiuser)>",
949 949 "description": "<description>",
950 950 "copy_permissions": "<bool>",
951 951 "private": "<bool>",
952 952 "landing_rev": "<landing_rev>"
953 953 }
954 954
955 955 Example error output:
956 956
957 957 .. code-block:: bash
958 958
959 959 id : <id_given_in_input>
960 960 result: {
961 961 "msg": "Created fork of `<reponame>` as `<forkname>`",
962 962 "success": true,
963 963 "task": "<celery task id or None if done sync>"
964 964 }
965 965 error: null
966 966
967 967 """
968 968 if not has_superadmin_permission(apiuser):
969 969 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
970 970 raise JSONRPCForbidden()
971 971
972 972 repo = get_repo_or_error(repoid)
973 973 repo_name = repo.repo_name
974 974
975 975 (fork_name_cleaned,
976 976 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(
977 977 fork_name)
978 978
979 979 if not has_superadmin_permission(apiuser):
980 980 # check if we have at least read permission for
981 981 # this repo that we fork !
982 982 _perms = (
983 983 'repository.admin', 'repository.write', 'repository.read')
984 984 has_repo_permissions(apiuser, repoid, repo, _perms)
985 985
986 986 if not isinstance(owner, Optional):
987 987 # forbid setting owner for non super admins
988 988 raise JSONRPCError(
989 989 'Only RhodeCode admin can specify `owner` param'
990 990 )
991 991 # check if we have a create.repo permission if not maybe the parent
992 992 # group permission
993 993 if not HasPermissionAnyApi('hg.create.repository')(user=apiuser):
994 994 if parent_group_name:
995 995 repogroupid = parent_group_name
996 996 repo_group = get_repo_group_or_error(parent_group_name)
997 997
998 998 _perms = ('group.admin',)
999 999 if not HasRepoGroupPermissionAnyApi(*_perms)(
1000 1000 user=apiuser, group_name=repo_group.group_name):
1001 1001 raise JSONRPCError(
1002 1002 'repository group `%s` does not exist' % (
1003 1003 repogroupid,))
1004 1004 else:
1005 1005 raise JSONRPCForbidden()
1006 1006
1007 1007 _repo = RepoModel().get_by_repo_name(fork_name)
1008 1008 if _repo:
1009 1009 type_ = 'fork' if _repo.fork else 'repo'
1010 1010 raise JSONRPCError("%s `%s` already exist" % (type_, fork_name))
1011 1011
1012 1012 if isinstance(owner, Optional):
1013 1013 owner = apiuser.user_id
1014 1014
1015 1015 owner = get_user_or_error(owner)
1016 1016
1017 1017 try:
1018 1018 # create structure of groups and return the last group
1019 1019 repo_group = map_groups(fork_name)
1020 1020 form_data = {
1021 1021 'repo_name': fork_name_cleaned,
1022 1022 'repo_name_full': fork_name,
1023 1023 'repo_group': repo_group.group_id if repo_group else None,
1024 1024 'repo_type': repo.repo_type,
1025 1025 'description': Optional.extract(description),
1026 1026 'private': Optional.extract(private),
1027 1027 'copy_permissions': Optional.extract(copy_permissions),
1028 1028 'landing_rev': Optional.extract(landing_rev),
1029 1029 'fork_parent_id': repo.repo_id,
1030 1030 }
1031 1031
1032 1032 task = RepoModel().create_fork(form_data, cur_user=owner)
1033 1033 # no commit, it's done in RepoModel, or async via celery
1034 1034 from celery.result import BaseAsyncResult
1035 1035 task_id = None
1036 1036 if isinstance(task, BaseAsyncResult):
1037 1037 task_id = task.task_id
1038 1038 return {
1039 1039 'msg': 'Created fork of `%s` as `%s`' % (
1040 1040 repo.repo_name, fork_name),
1041 1041 'success': True, # cannot return the repo data here since fork
1042 1042 # can be done async
1043 1043 'task': task_id
1044 1044 }
1045 1045 except Exception:
1046 1046 log.exception("Exception occurred while trying to fork a repo")
1047 1047 raise JSONRPCError(
1048 1048 'failed to fork repository `%s` as `%s`' % (
1049 1049 repo_name, fork_name))
1050 1050
1051 1051
1052 1052 @jsonrpc_method()
1053 1053 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1054 1054 """
1055 1055 Deletes a repository.
1056 1056
1057 1057 * When the `forks` parameter is set it's possible to detach or delete
1058 1058 forks of deleted repository.
1059 1059
1060 1060 This command can only be run using an |authtoken| with admin
1061 1061 permissions on the |repo|.
1062 1062
1063 1063 :param apiuser: This is filled automatically from the |authtoken|.
1064 1064 :type apiuser: AuthUser
1065 1065 :param repoid: Set the repository name or repository ID.
1066 1066 :type repoid: str or int
1067 1067 :param forks: Set to `detach` or `delete` forks from the |repo|.
1068 1068 :type forks: Optional(str)
1069 1069
1070 1070 Example error output:
1071 1071
1072 1072 .. code-block:: bash
1073 1073
1074 1074 id : <id_given_in_input>
1075 1075 result: {
1076 1076 "msg": "Deleted repository `<reponame>`",
1077 1077 "success": true
1078 1078 }
1079 1079 error: null
1080 1080 """
1081 1081
1082 1082 repo = get_repo_or_error(repoid)
1083 1083 if not has_superadmin_permission(apiuser):
1084 1084 _perms = ('repository.admin',)
1085 1085 has_repo_permissions(apiuser, repoid, repo, _perms)
1086 1086
1087 1087 try:
1088 1088 handle_forks = Optional.extract(forks)
1089 1089 _forks_msg = ''
1090 1090 _forks = [f for f in repo.forks]
1091 1091 if handle_forks == 'detach':
1092 1092 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1093 1093 elif handle_forks == 'delete':
1094 1094 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1095 1095 elif _forks:
1096 1096 raise JSONRPCError(
1097 1097 'Cannot delete `%s` it still contains attached forks' %
1098 1098 (repo.repo_name,)
1099 1099 )
1100 1100
1101 1101 RepoModel().delete(repo, forks=forks)
1102 1102 Session().commit()
1103 1103 return {
1104 1104 'msg': 'Deleted repository `%s`%s' % (
1105 1105 repo.repo_name, _forks_msg),
1106 1106 'success': True
1107 1107 }
1108 1108 except Exception:
1109 1109 log.exception("Exception occurred while trying to delete repo")
1110 1110 raise JSONRPCError(
1111 1111 'failed to delete repository `%s`' % (repo.repo_name,)
1112 1112 )
1113 1113
1114 1114
1115 1115 #TODO: marcink, change name ?
1116 1116 @jsonrpc_method()
1117 1117 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1118 1118 """
1119 1119 Invalidates the cache for the specified repository.
1120 1120
1121 1121 This command can only be run using an |authtoken| with admin rights to
1122 1122 the specified repository.
1123 1123
1124 1124 This command takes the following options:
1125 1125
1126 1126 :param apiuser: This is filled automatically from |authtoken|.
1127 1127 :type apiuser: AuthUser
1128 1128 :param repoid: Sets the repository name or repository ID.
1129 1129 :type repoid: str or int
1130 1130 :param delete_keys: This deletes the invalidated keys instead of
1131 1131 just flagging them.
1132 1132 :type delete_keys: Optional(``True`` | ``False``)
1133 1133
1134 1134 Example output:
1135 1135
1136 1136 .. code-block:: bash
1137 1137
1138 1138 id : <id_given_in_input>
1139 1139 result : {
1140 1140 'msg': Cache for repository `<repository name>` was invalidated,
1141 1141 'repository': <repository name>
1142 1142 }
1143 1143 error : null
1144 1144
1145 1145 Example error output:
1146 1146
1147 1147 .. code-block:: bash
1148 1148
1149 1149 id : <id_given_in_input>
1150 1150 result : null
1151 1151 error : {
1152 1152 'Error occurred during cache invalidation action'
1153 1153 }
1154 1154
1155 1155 """
1156 1156
1157 1157 repo = get_repo_or_error(repoid)
1158 1158 if not has_superadmin_permission(apiuser):
1159 1159 _perms = ('repository.admin', 'repository.write',)
1160 1160 has_repo_permissions(apiuser, repoid, repo, _perms)
1161 1161
1162 1162 delete = Optional.extract(delete_keys)
1163 1163 try:
1164 1164 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1165 1165 return {
1166 1166 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1167 1167 'repository': repo.repo_name
1168 1168 }
1169 1169 except Exception:
1170 1170 log.exception(
1171 1171 "Exception occurred while trying to invalidate repo cache")
1172 1172 raise JSONRPCError(
1173 1173 'Error occurred during cache invalidation action'
1174 1174 )
1175 1175
1176 1176
1177 1177 #TODO: marcink, change name ?
1178 1178 @jsonrpc_method()
1179 1179 def lock(request, apiuser, repoid, locked=Optional(None),
1180 1180 userid=Optional(OAttr('apiuser'))):
1181 1181 """
1182 1182 Sets the lock state of the specified |repo| by the given user.
1183 1183 From more information, see :ref:`repo-locking`.
1184 1184
1185 1185 * If the ``userid`` option is not set, the repository is locked to the
1186 1186 user who called the method.
1187 1187 * If the ``locked`` parameter is not set, the current lock state of the
1188 1188 repository is displayed.
1189 1189
1190 1190 This command can only be run using an |authtoken| with admin rights to
1191 1191 the specified repository.
1192 1192
1193 1193 This command takes the following options:
1194 1194
1195 1195 :param apiuser: This is filled automatically from the |authtoken|.
1196 1196 :type apiuser: AuthUser
1197 1197 :param repoid: Sets the repository name or repository ID.
1198 1198 :type repoid: str or int
1199 1199 :param locked: Sets the lock state.
1200 1200 :type locked: Optional(``True`` | ``False``)
1201 1201 :param userid: Set the repository lock to this user.
1202 1202 :type userid: Optional(str or int)
1203 1203
1204 1204 Example error output:
1205 1205
1206 1206 .. code-block:: bash
1207 1207
1208 1208 id : <id_given_in_input>
1209 1209 result : {
1210 1210 'repo': '<reponame>',
1211 1211 'locked': <bool: lock state>,
1212 1212 'locked_since': <int: lock timestamp>,
1213 1213 'locked_by': <username of person who made the lock>,
1214 1214 'lock_reason': <str: reason for locking>,
1215 1215 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1216 1216 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1217 1217 or
1218 1218 'msg': 'Repo `<repository name>` not locked.'
1219 1219 or
1220 1220 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1221 1221 }
1222 1222 error : null
1223 1223
1224 1224 Example error output:
1225 1225
1226 1226 .. code-block:: bash
1227 1227
1228 1228 id : <id_given_in_input>
1229 1229 result : null
1230 1230 error : {
1231 1231 'Error occurred locking repository `<reponame>`
1232 1232 }
1233 1233 """
1234 1234
1235 1235 repo = get_repo_or_error(repoid)
1236 1236 if not has_superadmin_permission(apiuser):
1237 1237 # check if we have at least write permission for this repo !
1238 1238 _perms = ('repository.admin', 'repository.write',)
1239 1239 has_repo_permissions(apiuser, repoid, repo, _perms)
1240 1240
1241 1241 # make sure normal user does not pass someone else userid,
1242 1242 # he is not allowed to do that
1243 1243 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1244 1244 raise JSONRPCError('userid is not the same as your user')
1245 1245
1246 1246 if isinstance(userid, Optional):
1247 1247 userid = apiuser.user_id
1248 1248
1249 1249 user = get_user_or_error(userid)
1250 1250
1251 1251 if isinstance(locked, Optional):
1252 1252 lockobj = repo.locked
1253 1253
1254 1254 if lockobj[0] is None:
1255 1255 _d = {
1256 1256 'repo': repo.repo_name,
1257 1257 'locked': False,
1258 1258 'locked_since': None,
1259 1259 'locked_by': None,
1260 1260 'lock_reason': None,
1261 1261 'lock_state_changed': False,
1262 1262 'msg': 'Repo `%s` not locked.' % repo.repo_name
1263 1263 }
1264 1264 return _d
1265 1265 else:
1266 1266 _user_id, _time, _reason = lockobj
1267 1267 lock_user = get_user_or_error(userid)
1268 1268 _d = {
1269 1269 'repo': repo.repo_name,
1270 1270 'locked': True,
1271 1271 'locked_since': _time,
1272 1272 'locked_by': lock_user.username,
1273 1273 'lock_reason': _reason,
1274 1274 'lock_state_changed': False,
1275 1275 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1276 1276 % (repo.repo_name, lock_user.username,
1277 1277 json.dumps(time_to_datetime(_time))))
1278 1278 }
1279 1279 return _d
1280 1280
1281 1281 # force locked state through a flag
1282 1282 else:
1283 1283 locked = str2bool(locked)
1284 1284 lock_reason = Repository.LOCK_API
1285 1285 try:
1286 1286 if locked:
1287 1287 lock_time = time.time()
1288 1288 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1289 1289 else:
1290 1290 lock_time = None
1291 1291 Repository.unlock(repo)
1292 1292 _d = {
1293 1293 'repo': repo.repo_name,
1294 1294 'locked': locked,
1295 1295 'locked_since': lock_time,
1296 1296 'locked_by': user.username,
1297 1297 'lock_reason': lock_reason,
1298 1298 'lock_state_changed': True,
1299 1299 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1300 1300 % (user.username, repo.repo_name, locked))
1301 1301 }
1302 1302 return _d
1303 1303 except Exception:
1304 1304 log.exception(
1305 1305 "Exception occurred while trying to lock repository")
1306 1306 raise JSONRPCError(
1307 1307 'Error occurred locking repository `%s`' % repo.repo_name
1308 1308 )
1309 1309
1310 1310
1311 1311 @jsonrpc_method()
1312 1312 def comment_commit(
1313 1313 request, apiuser, repoid, commit_id, message,
1314 1314 userid=Optional(OAttr('apiuser')), status=Optional(None)):
1315 1315 """
1316 1316 Set a commit comment, and optionally change the status of the commit.
1317 1317
1318 1318 :param apiuser: This is filled automatically from the |authtoken|.
1319 1319 :type apiuser: AuthUser
1320 1320 :param repoid: Set the repository name or repository ID.
1321 1321 :type repoid: str or int
1322 1322 :param commit_id: Specify the commit_id for which to set a comment.
1323 1323 :type commit_id: str
1324 1324 :param message: The comment text.
1325 1325 :type message: str
1326 1326 :param userid: Set the user name of the comment creator.
1327 1327 :type userid: Optional(str or int)
1328 1328 :param status: status, one of 'not_reviewed', 'approved', 'rejected',
1329 1329 'under_review'
1330 1330 :type status: str
1331 1331
1332 1332 Example error output:
1333 1333
1334 1334 .. code-block:: json
1335 1335
1336 1336 {
1337 1337 "id" : <id_given_in_input>,
1338 1338 "result" : {
1339 1339 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1340 1340 "status_change": null or <status>,
1341 1341 "success": true
1342 1342 },
1343 1343 "error" : null
1344 1344 }
1345 1345
1346 1346 """
1347 1347 repo = get_repo_or_error(repoid)
1348 1348 if not has_superadmin_permission(apiuser):
1349 1349 _perms = ('repository.read', 'repository.write', 'repository.admin')
1350 1350 has_repo_permissions(apiuser, repoid, repo, _perms)
1351 1351
1352 1352 if isinstance(userid, Optional):
1353 1353 userid = apiuser.user_id
1354 1354
1355 1355 user = get_user_or_error(userid)
1356 1356 status = Optional.extract(status)
1357 1357
1358 1358 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1359 1359 if status and status not in allowed_statuses:
1360 1360 raise JSONRPCError('Bad status, must be on '
1361 1361 'of %s got %s' % (allowed_statuses, status,))
1362 1362
1363 1363 try:
1364 1364 rc_config = SettingsModel().get_all_settings()
1365 1365 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1366 1366
1367 1367 comm = ChangesetCommentsModel().create(
1368 1368 message, repo, user, revision=commit_id, status_change=status,
1369 1369 renderer=renderer)
1370 1370 if status:
1371 1371 # also do a status change
1372 1372 try:
1373 1373 ChangesetStatusModel().set_status(
1374 1374 repo, status, user, comm, revision=commit_id,
1375 1375 dont_allow_on_closed_pull_request=True
1376 1376 )
1377 1377 except StatusChangeOnClosedPullRequestError:
1378 1378 log.exception(
1379 1379 "Exception occurred while trying to change repo commit status")
1380 1380 msg = ('Changing status on a changeset associated with '
1381 1381 'a closed pull request is not allowed')
1382 1382 raise JSONRPCError(msg)
1383 1383
1384 1384 Session().commit()
1385 1385 return {
1386 1386 'msg': (
1387 1387 'Commented on commit `%s` for repository `%s`' % (
1388 1388 comm.revision, repo.repo_name)),
1389 1389 'status_change': status,
1390 1390 'success': True,
1391 1391 }
1392 1392 except JSONRPCError:
1393 1393 # catch any inside errors, and re-raise them to prevent from
1394 1394 # below global catch to silence them
1395 1395 raise
1396 1396 except Exception:
1397 1397 log.exception("Exception occurred while trying to comment on commit")
1398 1398 raise JSONRPCError(
1399 1399 'failed to set comment on repository `%s`' % (repo.repo_name,)
1400 1400 )
1401 1401
1402 1402
1403 1403 @jsonrpc_method()
1404 1404 def grant_user_permission(request, apiuser, repoid, userid, perm):
1405 1405 """
1406 1406 Grant permissions for the specified user on the given repository,
1407 1407 or update existing permissions if found.
1408 1408
1409 1409 This command can only be run using an |authtoken| with admin
1410 1410 permissions on the |repo|.
1411 1411
1412 1412 :param apiuser: This is filled automatically from the |authtoken|.
1413 1413 :type apiuser: AuthUser
1414 1414 :param repoid: Set the repository name or repository ID.
1415 1415 :type repoid: str or int
1416 1416 :param userid: Set the user name.
1417 1417 :type userid: str
1418 1418 :param perm: Set the user permissions, using the following format
1419 1419 ``(repository.(none|read|write|admin))``
1420 1420 :type perm: str
1421 1421
1422 1422 Example output:
1423 1423
1424 1424 .. code-block:: bash
1425 1425
1426 1426 id : <id_given_in_input>
1427 1427 result: {
1428 1428 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1429 1429 "success": true
1430 1430 }
1431 1431 error: null
1432 1432 """
1433 1433
1434 1434 repo = get_repo_or_error(repoid)
1435 1435 user = get_user_or_error(userid)
1436 1436 perm = get_perm_or_error(perm)
1437 1437 if not has_superadmin_permission(apiuser):
1438 1438 _perms = ('repository.admin',)
1439 1439 has_repo_permissions(apiuser, repoid, repo, _perms)
1440 1440
1441 1441 try:
1442 1442
1443 1443 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
1444 1444
1445 1445 Session().commit()
1446 1446 return {
1447 1447 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1448 1448 perm.permission_name, user.username, repo.repo_name
1449 1449 ),
1450 1450 'success': True
1451 1451 }
1452 1452 except Exception:
1453 1453 log.exception(
1454 1454 "Exception occurred while trying edit permissions for repo")
1455 1455 raise JSONRPCError(
1456 1456 'failed to edit permission for user: `%s` in repo: `%s`' % (
1457 1457 userid, repoid
1458 1458 )
1459 1459 )
1460 1460
1461 1461
1462 1462 @jsonrpc_method()
1463 1463 def revoke_user_permission(request, apiuser, repoid, userid):
1464 1464 """
1465 1465 Revoke permission for a user on the specified repository.
1466 1466
1467 1467 This command can only be run using an |authtoken| with admin
1468 1468 permissions on the |repo|.
1469 1469
1470 1470 :param apiuser: This is filled automatically from the |authtoken|.
1471 1471 :type apiuser: AuthUser
1472 1472 :param repoid: Set the repository name or repository ID.
1473 1473 :type repoid: str or int
1474 1474 :param userid: Set the user name of revoked user.
1475 1475 :type userid: str or int
1476 1476
1477 1477 Example error output:
1478 1478
1479 1479 .. code-block:: bash
1480 1480
1481 1481 id : <id_given_in_input>
1482 1482 result: {
1483 1483 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1484 1484 "success": true
1485 1485 }
1486 1486 error: null
1487 1487 """
1488 1488
1489 1489 repo = get_repo_or_error(repoid)
1490 1490 user = get_user_or_error(userid)
1491 1491 if not has_superadmin_permission(apiuser):
1492 1492 _perms = ('repository.admin',)
1493 1493 has_repo_permissions(apiuser, repoid, repo, _perms)
1494 1494
1495 1495 try:
1496 1496 RepoModel().revoke_user_permission(repo=repo, user=user)
1497 1497 Session().commit()
1498 1498 return {
1499 1499 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1500 1500 user.username, repo.repo_name
1501 1501 ),
1502 1502 'success': True
1503 1503 }
1504 1504 except Exception:
1505 1505 log.exception(
1506 1506 "Exception occurred while trying revoke permissions to repo")
1507 1507 raise JSONRPCError(
1508 1508 'failed to edit permission for user: `%s` in repo: `%s`' % (
1509 1509 userid, repoid
1510 1510 )
1511 1511 )
1512 1512
1513 1513
1514 1514 @jsonrpc_method()
1515 1515 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1516 1516 """
1517 1517 Grant permission for a user group on the specified repository,
1518 1518 or update existing permissions.
1519 1519
1520 1520 This command can only be run using an |authtoken| with admin
1521 1521 permissions on the |repo|.
1522 1522
1523 1523 :param apiuser: This is filled automatically from the |authtoken|.
1524 1524 :type apiuser: AuthUser
1525 1525 :param repoid: Set the repository name or repository ID.
1526 1526 :type repoid: str or int
1527 1527 :param usergroupid: Specify the ID of the user group.
1528 1528 :type usergroupid: str or int
1529 1529 :param perm: Set the user group permissions using the following
1530 1530 format: (repository.(none|read|write|admin))
1531 1531 :type perm: str
1532 1532
1533 1533 Example output:
1534 1534
1535 1535 .. code-block:: bash
1536 1536
1537 1537 id : <id_given_in_input>
1538 1538 result : {
1539 1539 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1540 1540 "success": true
1541 1541
1542 1542 }
1543 1543 error : null
1544 1544
1545 1545 Example error output:
1546 1546
1547 1547 .. code-block:: bash
1548 1548
1549 1549 id : <id_given_in_input>
1550 1550 result : null
1551 1551 error : {
1552 1552 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1553 1553 }
1554 1554
1555 1555 """
1556 1556
1557 1557 repo = get_repo_or_error(repoid)
1558 1558 perm = get_perm_or_error(perm)
1559 1559 if not has_superadmin_permission(apiuser):
1560 1560 _perms = ('repository.admin',)
1561 1561 has_repo_permissions(apiuser, repoid, repo, _perms)
1562 1562
1563 1563 user_group = get_user_group_or_error(usergroupid)
1564 1564 if not has_superadmin_permission(apiuser):
1565 1565 # check if we have at least read permission for this user group !
1566 1566 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1567 1567 if not HasUserGroupPermissionAnyApi(*_perms)(
1568 1568 user=apiuser, user_group_name=user_group.users_group_name):
1569 1569 raise JSONRPCError(
1570 1570 'user group `%s` does not exist' % (usergroupid,))
1571 1571
1572 1572 try:
1573 1573 RepoModel().grant_user_group_permission(
1574 1574 repo=repo, group_name=user_group, perm=perm)
1575 1575
1576 1576 Session().commit()
1577 1577 return {
1578 1578 'msg': 'Granted perm: `%s` for user group: `%s` in '
1579 1579 'repo: `%s`' % (
1580 1580 perm.permission_name, user_group.users_group_name,
1581 1581 repo.repo_name
1582 1582 ),
1583 1583 'success': True
1584 1584 }
1585 1585 except Exception:
1586 1586 log.exception(
1587 1587 "Exception occurred while trying change permission on repo")
1588 1588 raise JSONRPCError(
1589 1589 'failed to edit permission for user group: `%s` in '
1590 1590 'repo: `%s`' % (
1591 1591 usergroupid, repo.repo_name
1592 1592 )
1593 1593 )
1594 1594
1595 1595
1596 1596 @jsonrpc_method()
1597 1597 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1598 1598 """
1599 1599 Revoke the permissions of a user group on a given repository.
1600 1600
1601 1601 This command can only be run using an |authtoken| with admin
1602 1602 permissions on the |repo|.
1603 1603
1604 1604 :param apiuser: This is filled automatically from the |authtoken|.
1605 1605 :type apiuser: AuthUser
1606 1606 :param repoid: Set the repository name or repository ID.
1607 1607 :type repoid: str or int
1608 1608 :param usergroupid: Specify the user group ID.
1609 1609 :type usergroupid: str or int
1610 1610
1611 1611 Example output:
1612 1612
1613 1613 .. code-block:: bash
1614 1614
1615 1615 id : <id_given_in_input>
1616 1616 result: {
1617 1617 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1618 1618 "success": true
1619 1619 }
1620 1620 error: null
1621 1621 """
1622 1622
1623 1623 repo = get_repo_or_error(repoid)
1624 1624 if not has_superadmin_permission(apiuser):
1625 1625 _perms = ('repository.admin',)
1626 1626 has_repo_permissions(apiuser, repoid, repo, _perms)
1627 1627
1628 1628 user_group = get_user_group_or_error(usergroupid)
1629 1629 if not has_superadmin_permission(apiuser):
1630 1630 # check if we have at least read permission for this user group !
1631 1631 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1632 1632 if not HasUserGroupPermissionAnyApi(*_perms)(
1633 1633 user=apiuser, user_group_name=user_group.users_group_name):
1634 1634 raise JSONRPCError(
1635 1635 'user group `%s` does not exist' % (usergroupid,))
1636 1636
1637 1637 try:
1638 1638 RepoModel().revoke_user_group_permission(
1639 1639 repo=repo, group_name=user_group)
1640 1640
1641 1641 Session().commit()
1642 1642 return {
1643 1643 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1644 1644 user_group.users_group_name, repo.repo_name
1645 1645 ),
1646 1646 'success': True
1647 1647 }
1648 1648 except Exception:
1649 1649 log.exception("Exception occurred while trying revoke "
1650 1650 "user group permission on repo")
1651 1651 raise JSONRPCError(
1652 1652 'failed to edit permission for user group: `%s` in '
1653 1653 'repo: `%s`' % (
1654 1654 user_group.users_group_name, repo.repo_name
1655 1655 )
1656 1656 )
1657 1657
1658 1658
1659 1659 @jsonrpc_method()
1660 1660 def pull(request, apiuser, repoid):
1661 1661 """
1662 1662 Triggers a pull on the given repository from a remote location. You
1663 1663 can use this to keep remote repositories up-to-date.
1664 1664
1665 1665 This command can only be run using an |authtoken| with admin
1666 1666 rights to the specified repository. For more information,
1667 1667 see :ref:`config-token-ref`.
1668 1668
1669 1669 This command takes the following options:
1670 1670
1671 1671 :param apiuser: This is filled automatically from the |authtoken|.
1672 1672 :type apiuser: AuthUser
1673 1673 :param repoid: The repository name or repository ID.
1674 1674 :type repoid: str or int
1675 1675
1676 1676 Example output:
1677 1677
1678 1678 .. code-block:: bash
1679 1679
1680 1680 id : <id_given_in_input>
1681 1681 result : {
1682 1682 "msg": "Pulled from `<repository name>`"
1683 1683 "repository": "<repository name>"
1684 1684 }
1685 1685 error : null
1686 1686
1687 1687 Example error output:
1688 1688
1689 1689 .. code-block:: bash
1690 1690
1691 1691 id : <id_given_in_input>
1692 1692 result : null
1693 1693 error : {
1694 1694 "Unable to pull changes from `<reponame>`"
1695 1695 }
1696 1696
1697 1697 """
1698 1698
1699 1699 repo = get_repo_or_error(repoid)
1700 1700 if not has_superadmin_permission(apiuser):
1701 1701 _perms = ('repository.admin',)
1702 1702 has_repo_permissions(apiuser, repoid, repo, _perms)
1703 1703
1704 1704 try:
1705 1705 ScmModel().pull_changes(repo.repo_name, apiuser.username)
1706 1706 return {
1707 1707 'msg': 'Pulled from `%s`' % repo.repo_name,
1708 1708 'repository': repo.repo_name
1709 1709 }
1710 1710 except Exception:
1711 1711 log.exception("Exception occurred while trying to "
1712 1712 "pull changes from remote location")
1713 1713 raise JSONRPCError(
1714 1714 'Unable to pull changes from `%s`' % repo.repo_name
1715 1715 )
1716 1716
1717 1717
1718 1718 @jsonrpc_method()
1719 1719 def strip(request, apiuser, repoid, revision, branch):
1720 1720 """
1721 1721 Strips the given revision from the specified repository.
1722 1722
1723 1723 * This will remove the revision and all of its decendants.
1724 1724
1725 1725 This command can only be run using an |authtoken| with admin rights to
1726 1726 the specified repository.
1727 1727
1728 1728 This command takes the following options:
1729 1729
1730 1730 :param apiuser: This is filled automatically from the |authtoken|.
1731 1731 :type apiuser: AuthUser
1732 1732 :param repoid: The repository name or repository ID.
1733 1733 :type repoid: str or int
1734 1734 :param revision: The revision you wish to strip.
1735 1735 :type revision: str
1736 1736 :param branch: The branch from which to strip the revision.
1737 1737 :type branch: str
1738 1738
1739 1739 Example output:
1740 1740
1741 1741 .. code-block:: bash
1742 1742
1743 1743 id : <id_given_in_input>
1744 1744 result : {
1745 1745 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
1746 1746 "repository": "<repository name>"
1747 1747 }
1748 1748 error : null
1749 1749
1750 1750 Example error output:
1751 1751
1752 1752 .. code-block:: bash
1753 1753
1754 1754 id : <id_given_in_input>
1755 1755 result : null
1756 1756 error : {
1757 1757 "Unable to strip commit <commit_hash> from repo `<repository name>`"
1758 1758 }
1759 1759
1760 1760 """
1761 1761
1762 1762 repo = get_repo_or_error(repoid)
1763 1763 if not has_superadmin_permission(apiuser):
1764 1764 _perms = ('repository.admin',)
1765 1765 has_repo_permissions(apiuser, repoid, repo, _perms)
1766 1766
1767 1767 try:
1768 1768 ScmModel().strip(repo, revision, branch)
1769 1769 return {
1770 1770 'msg': 'Stripped commit %s from repo `%s`' % (
1771 1771 revision, repo.repo_name),
1772 1772 'repository': repo.repo_name
1773 1773 }
1774 1774 except Exception:
1775 1775 log.exception("Exception while trying to strip")
1776 1776 raise JSONRPCError(
1777 1777 'Unable to strip commit %s from repo `%s`' % (
1778 1778 revision, repo.repo_name)
1779 1779 )
1780 1780
1781 1781
1782 1782 @jsonrpc_method()
1783 1783 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
1784 1784 """
1785 1785 Returns all settings for a repository. If key is given it only returns the
1786 1786 setting identified by the key or null.
1787 1787
1788 1788 :param apiuser: This is filled automatically from the |authtoken|.
1789 1789 :type apiuser: AuthUser
1790 1790 :param repoid: The repository name or repository id.
1791 1791 :type repoid: str or int
1792 1792 :param key: Key of the setting to return.
1793 1793 :type: key: Optional(str)
1794 1794
1795 1795 Example output:
1796 1796
1797 1797 .. code-block:: bash
1798 1798
1799 1799 {
1800 1800 "error": null,
1801 1801 "id": 237,
1802 1802 "result": {
1803 1803 "extensions_largefiles": true,
1804 1804 "hooks_changegroup_push_logger": true,
1805 1805 "hooks_changegroup_repo_size": false,
1806 1806 "hooks_outgoing_pull_logger": true,
1807 1807 "phases_publish": "True",
1808 1808 "rhodecode_hg_use_rebase_for_merging": true,
1809 1809 "rhodecode_pr_merge_enabled": true,
1810 1810 "rhodecode_use_outdated_comments": true
1811 1811 }
1812 1812 }
1813 1813 """
1814 1814
1815 1815 # Restrict access to this api method to admins only.
1816 1816 if not has_superadmin_permission(apiuser):
1817 1817 raise JSONRPCForbidden()
1818 1818
1819 1819 try:
1820 1820 repo = get_repo_or_error(repoid)
1821 1821 settings_model = VcsSettingsModel(repo=repo)
1822 1822 settings = settings_model.get_global_settings()
1823 1823 settings.update(settings_model.get_repo_settings())
1824 1824
1825 1825 # If only a single setting is requested fetch it from all settings.
1826 1826 key = Optional.extract(key)
1827 1827 if key is not None:
1828 1828 settings = settings.get(key, None)
1829 1829 except Exception:
1830 1830 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
1831 1831 log.exception(msg)
1832 1832 raise JSONRPCError(msg)
1833 1833
1834 1834 return settings
1835 1835
1836 1836
1837 1837 @jsonrpc_method()
1838 1838 def set_repo_settings(request, apiuser, repoid, settings):
1839 1839 """
1840 1840 Update repository settings. Returns true on success.
1841 1841
1842 1842 :param apiuser: This is filled automatically from the |authtoken|.
1843 1843 :type apiuser: AuthUser
1844 1844 :param repoid: The repository name or repository id.
1845 1845 :type repoid: str or int
1846 1846 :param settings: The new settings for the repository.
1847 1847 :type: settings: dict
1848 1848
1849 1849 Example output:
1850 1850
1851 1851 .. code-block:: bash
1852 1852
1853 1853 {
1854 1854 "error": null,
1855 1855 "id": 237,
1856 1856 "result": true
1857 1857 }
1858 1858 """
1859 1859 # Restrict access to this api method to admins only.
1860 1860 if not has_superadmin_permission(apiuser):
1861 1861 raise JSONRPCForbidden()
1862 1862
1863 1863 if type(settings) is not dict:
1864 1864 raise JSONRPCError('Settings have to be a JSON Object.')
1865 1865
1866 1866 try:
1867 1867 settings_model = VcsSettingsModel(repo=repoid)
1868 1868
1869 1869 # Merge global, repo and incoming settings.
1870 1870 new_settings = settings_model.get_global_settings()
1871 1871 new_settings.update(settings_model.get_repo_settings())
1872 1872 new_settings.update(settings)
1873 1873
1874 1874 # Update the settings.
1875 1875 inherit_global_settings = new_settings.get(
1876 1876 'inherit_global_settings', False)
1877 1877 settings_model.create_or_update_repo_settings(
1878 1878 new_settings, inherit_global_settings=inherit_global_settings)
1879 1879 Session().commit()
1880 1880 except Exception:
1881 1881 msg = 'Failed to update settings for repository `{}`'.format(repoid)
1882 1882 log.exception(msg)
1883 1883 raise JSONRPCError(msg)
1884 1884
1885 1885 # Indicate success.
1886 1886 return True
@@ -1,699 +1,699 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import logging
23 23
24 24 import colander
25 25
26 26 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCForbidden
27 27 from rhodecode.api.utils import (
28 28 has_superadmin_permission, Optional, OAttr, get_user_or_error,
29 29 store_update, get_repo_group_or_error,
30 30 get_perm_or_error, get_user_group_or_error, get_origin)
31 31 from rhodecode.lib.auth import (
32 32 HasPermissionAnyApi, HasRepoGroupPermissionAnyApi,
33 33 HasUserGroupPermissionAnyApi)
34 34 from rhodecode.model.db import Session, RepoGroup
35 35 from rhodecode.model.repo_group import RepoGroupModel
36 36 from rhodecode.model.scm import RepoGroupList
37 from rhodecode.model.validation_schema import RepoGroupSchema
37 from rhodecode.model.validation_schema.schemas import repo_group_schema
38 38
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 @jsonrpc_method()
44 44 def get_repo_group(request, apiuser, repogroupid):
45 45 """
46 46 Return the specified |repo| group, along with permissions,
47 47 and repositories inside the group
48 48
49 49 :param apiuser: This is filled automatically from the |authtoken|.
50 50 :type apiuser: AuthUser
51 51 :param repogroupid: Specify the name of ID of the repository group.
52 52 :type repogroupid: str or int
53 53
54 54
55 55 Example output:
56 56
57 57 .. code-block:: bash
58 58
59 59 {
60 60 "error": null,
61 61 "id": repo-group-id,
62 62 "result": {
63 63 "group_description": "repo group description",
64 64 "group_id": 14,
65 65 "group_name": "group name",
66 66 "members": [
67 67 {
68 68 "name": "super-admin-username",
69 69 "origin": "super-admin",
70 70 "permission": "group.admin",
71 71 "type": "user"
72 72 },
73 73 {
74 74 "name": "owner-name",
75 75 "origin": "owner",
76 76 "permission": "group.admin",
77 77 "type": "user"
78 78 },
79 79 {
80 80 "name": "user-group-name",
81 81 "origin": "permission",
82 82 "permission": "group.write",
83 83 "type": "user_group"
84 84 }
85 85 ],
86 86 "owner": "owner-name",
87 87 "parent_group": null,
88 88 "repositories": [ repo-list ]
89 89 }
90 90 }
91 91 """
92 92
93 93 repo_group = get_repo_group_or_error(repogroupid)
94 94 if not has_superadmin_permission(apiuser):
95 95 # check if we have at least read permission for this repo group !
96 96 _perms = ('group.admin', 'group.write', 'group.read',)
97 97 if not HasRepoGroupPermissionAnyApi(*_perms)(
98 98 user=apiuser, group_name=repo_group.group_name):
99 99 raise JSONRPCError(
100 100 'repository group `%s` does not exist' % (repogroupid,))
101 101
102 102 permissions = []
103 103 for _user in repo_group.permissions():
104 104 user_data = {
105 105 'name': _user.username,
106 106 'permission': _user.permission,
107 107 'origin': get_origin(_user),
108 108 'type': "user",
109 109 }
110 110 permissions.append(user_data)
111 111
112 112 for _user_group in repo_group.permission_user_groups():
113 113 user_group_data = {
114 114 'name': _user_group.users_group_name,
115 115 'permission': _user_group.permission,
116 116 'origin': get_origin(_user_group),
117 117 'type': "user_group",
118 118 }
119 119 permissions.append(user_group_data)
120 120
121 121 data = repo_group.get_api_data()
122 122 data["members"] = permissions # TODO: this should be named permissions
123 123 return data
124 124
125 125
126 126 @jsonrpc_method()
127 127 def get_repo_groups(request, apiuser):
128 128 """
129 129 Returns all repository groups.
130 130
131 131 :param apiuser: This is filled automatically from the |authtoken|.
132 132 :type apiuser: AuthUser
133 133 """
134 134
135 135 result = []
136 136 _perms = ('group.read', 'group.write', 'group.admin',)
137 137 extras = {'user': apiuser}
138 138 for repo_group in RepoGroupList(RepoGroupModel().get_all(),
139 139 perm_set=_perms, extra_kwargs=extras):
140 140 result.append(repo_group.get_api_data())
141 141 return result
142 142
143 143
144 144 @jsonrpc_method()
145 145 def create_repo_group(request, apiuser, group_name, description=Optional(''),
146 146 owner=Optional(OAttr('apiuser')),
147 147 copy_permissions=Optional(False)):
148 148 """
149 149 Creates a repository group.
150 150
151 151 * If the repository group name contains "/", all the required repository
152 152 groups will be created.
153 153
154 154 For example "foo/bar/baz" will create |repo| groups "foo" and "bar"
155 155 (with "foo" as parent). It will also create the "baz" repository
156 156 with "bar" as |repo| group.
157 157
158 158 This command can only be run using an |authtoken| with admin
159 159 permissions.
160 160
161 161 :param apiuser: This is filled automatically from the |authtoken|.
162 162 :type apiuser: AuthUser
163 163 :param group_name: Set the repository group name.
164 164 :type group_name: str
165 165 :param description: Set the |repo| group description.
166 166 :type description: str
167 167 :param owner: Set the |repo| group owner.
168 168 :type owner: str
169 169 :param copy_permissions:
170 170 :type copy_permissions:
171 171
172 172 Example output:
173 173
174 174 .. code-block:: bash
175 175
176 176 id : <id_given_in_input>
177 177 result : {
178 178 "msg": "Created new repo group `<repo_group_name>`"
179 179 "repo_group": <repogroup_object>
180 180 }
181 181 error : null
182 182
183 183
184 184 Example error output:
185 185
186 186 .. code-block:: bash
187 187
188 188 id : <id_given_in_input>
189 189 result : null
190 190 error : {
191 191 failed to create repo group `<repogroupid>`
192 192 }
193 193
194 194 """
195 195
196 schema = RepoGroupSchema()
196 schema = repo_group_schema.RepoGroupSchema()
197 197 try:
198 198 data = schema.deserialize({
199 199 'group_name': group_name
200 200 })
201 201 except colander.Invalid as e:
202 202 raise JSONRPCError("Validation failed: %s" % (e.asdict(),))
203 203 group_name = data['group_name']
204 204
205 205 if isinstance(owner, Optional):
206 206 owner = apiuser.user_id
207 207
208 208 group_description = Optional.extract(description)
209 209 copy_permissions = Optional.extract(copy_permissions)
210 210
211 211 # get by full name with parents, check if it already exist
212 212 if RepoGroup.get_by_group_name(group_name):
213 213 raise JSONRPCError("repo group `%s` already exist" % (group_name,))
214 214
215 215 (group_name_cleaned,
216 216 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(
217 217 group_name)
218 218
219 219 parent_group = None
220 220 if parent_group_name:
221 221 parent_group = get_repo_group_or_error(parent_group_name)
222 222
223 223 if not HasPermissionAnyApi(
224 224 'hg.admin', 'hg.repogroup.create.true')(user=apiuser):
225 225 # check if we have admin permission for this parent repo group !
226 226 # users without admin or hg.repogroup.create can only create other
227 227 # groups in groups they own so this is a required, but can be empty
228 228 parent_group = getattr(parent_group, 'group_name', '')
229 229 _perms = ('group.admin',)
230 230 if not HasRepoGroupPermissionAnyApi(*_perms)(
231 231 user=apiuser, group_name=parent_group):
232 232 raise JSONRPCForbidden()
233 233
234 234 try:
235 235 repo_group = RepoGroupModel().create(
236 236 group_name=group_name,
237 237 group_description=group_description,
238 238 owner=owner,
239 239 copy_permissions=copy_permissions)
240 240 Session().commit()
241 241 return {
242 242 'msg': 'Created new repo group `%s`' % group_name,
243 243 'repo_group': repo_group.get_api_data()
244 244 }
245 245 except Exception:
246 246 log.exception("Exception occurred while trying create repo group")
247 247 raise JSONRPCError(
248 248 'failed to create repo group `%s`' % (group_name,))
249 249
250 250
251 251 @jsonrpc_method()
252 252 def update_repo_group(
253 253 request, apiuser, repogroupid, group_name=Optional(''),
254 254 description=Optional(''), owner=Optional(OAttr('apiuser')),
255 255 parent=Optional(None), enable_locking=Optional(False)):
256 256 """
257 257 Updates repository group with the details given.
258 258
259 259 This command can only be run using an |authtoken| with admin
260 260 permissions.
261 261
262 262 :param apiuser: This is filled automatically from the |authtoken|.
263 263 :type apiuser: AuthUser
264 264 :param repogroupid: Set the ID of repository group.
265 265 :type repogroupid: str or int
266 266 :param group_name: Set the name of the |repo| group.
267 267 :type group_name: str
268 268 :param description: Set a description for the group.
269 269 :type description: str
270 270 :param owner: Set the |repo| group owner.
271 271 :type owner: str
272 272 :param parent: Set the |repo| group parent.
273 273 :type parent: str or int
274 274 :param enable_locking: Enable |repo| locking. The default is false.
275 275 :type enable_locking: bool
276 276 """
277 277
278 278 repo_group = get_repo_group_or_error(repogroupid)
279 279 if not has_superadmin_permission(apiuser):
280 280 # check if we have admin permission for this repo group !
281 281 _perms = ('group.admin',)
282 282 if not HasRepoGroupPermissionAnyApi(*_perms)(
283 283 user=apiuser, group_name=repo_group.group_name):
284 284 raise JSONRPCError(
285 285 'repository group `%s` does not exist' % (repogroupid,))
286 286
287 287 updates = {}
288 288 try:
289 289 store_update(updates, group_name, 'group_name')
290 290 store_update(updates, description, 'group_description')
291 291 store_update(updates, owner, 'user')
292 292 store_update(updates, parent, 'group_parent_id')
293 293 store_update(updates, enable_locking, 'enable_locking')
294 294 repo_group = RepoGroupModel().update(repo_group, updates)
295 295 Session().commit()
296 296 return {
297 297 'msg': 'updated repository group ID:%s %s' % (
298 298 repo_group.group_id, repo_group.group_name),
299 299 'repo_group': repo_group.get_api_data()
300 300 }
301 301 except Exception:
302 302 log.exception("Exception occurred while trying update repo group")
303 303 raise JSONRPCError('failed to update repository group `%s`'
304 304 % (repogroupid,))
305 305
306 306
307 307 @jsonrpc_method()
308 308 def delete_repo_group(request, apiuser, repogroupid):
309 309 """
310 310 Deletes a |repo| group.
311 311
312 312 :param apiuser: This is filled automatically from the |authtoken|.
313 313 :type apiuser: AuthUser
314 314 :param repogroupid: Set the name or ID of repository group to be
315 315 deleted.
316 316 :type repogroupid: str or int
317 317
318 318 Example output:
319 319
320 320 .. code-block:: bash
321 321
322 322 id : <id_given_in_input>
323 323 result : {
324 324 'msg': 'deleted repo group ID:<repogroupid> <repogroupname>
325 325 'repo_group': null
326 326 }
327 327 error : null
328 328
329 329 Example error output:
330 330
331 331 .. code-block:: bash
332 332
333 333 id : <id_given_in_input>
334 334 result : null
335 335 error : {
336 336 "failed to delete repo group ID:<repogroupid> <repogroupname>"
337 337 }
338 338
339 339 """
340 340
341 341 repo_group = get_repo_group_or_error(repogroupid)
342 342 if not has_superadmin_permission(apiuser):
343 343 # check if we have admin permission for this repo group !
344 344 _perms = ('group.admin',)
345 345 if not HasRepoGroupPermissionAnyApi(*_perms)(
346 346 user=apiuser, group_name=repo_group.group_name):
347 347 raise JSONRPCError(
348 348 'repository group `%s` does not exist' % (repogroupid,))
349 349 try:
350 350 RepoGroupModel().delete(repo_group)
351 351 Session().commit()
352 352 return {
353 353 'msg': 'deleted repo group ID:%s %s' %
354 354 (repo_group.group_id, repo_group.group_name),
355 355 'repo_group': None
356 356 }
357 357 except Exception:
358 358 log.exception("Exception occurred while trying to delete repo group")
359 359 raise JSONRPCError('failed to delete repo group ID:%s %s' %
360 360 (repo_group.group_id, repo_group.group_name))
361 361
362 362
363 363 @jsonrpc_method()
364 364 def grant_user_permission_to_repo_group(
365 365 request, apiuser, repogroupid, userid, perm,
366 366 apply_to_children=Optional('none')):
367 367 """
368 368 Grant permission for a user on the given repository group, or update
369 369 existing permissions if found.
370 370
371 371 This command can only be run using an |authtoken| with admin
372 372 permissions.
373 373
374 374 :param apiuser: This is filled automatically from the |authtoken|.
375 375 :type apiuser: AuthUser
376 376 :param repogroupid: Set the name or ID of repository group.
377 377 :type repogroupid: str or int
378 378 :param userid: Set the user name.
379 379 :type userid: str
380 380 :param perm: (group.(none|read|write|admin))
381 381 :type perm: str
382 382 :param apply_to_children: 'none', 'repos', 'groups', 'all'
383 383 :type apply_to_children: str
384 384
385 385 Example output:
386 386
387 387 .. code-block:: bash
388 388
389 389 id : <id_given_in_input>
390 390 result: {
391 391 "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`",
392 392 "success": true
393 393 }
394 394 error: null
395 395
396 396 Example error output:
397 397
398 398 .. code-block:: bash
399 399
400 400 id : <id_given_in_input>
401 401 result : null
402 402 error : {
403 403 "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`"
404 404 }
405 405
406 406 """
407 407
408 408 repo_group = get_repo_group_or_error(repogroupid)
409 409
410 410 if not has_superadmin_permission(apiuser):
411 411 # check if we have admin permission for this repo group !
412 412 _perms = ('group.admin',)
413 413 if not HasRepoGroupPermissionAnyApi(*_perms)(
414 414 user=apiuser, group_name=repo_group.group_name):
415 415 raise JSONRPCError(
416 416 'repository group `%s` does not exist' % (repogroupid,))
417 417
418 418 user = get_user_or_error(userid)
419 419 perm = get_perm_or_error(perm, prefix='group.')
420 420 apply_to_children = Optional.extract(apply_to_children)
421 421
422 422 perm_additions = [[user.user_id, perm, "user"]]
423 423 try:
424 424 RepoGroupModel().update_permissions(repo_group=repo_group,
425 425 perm_additions=perm_additions,
426 426 recursive=apply_to_children,
427 427 cur_user=apiuser)
428 428 Session().commit()
429 429 return {
430 430 'msg': 'Granted perm: `%s` (recursive:%s) for user: '
431 431 '`%s` in repo group: `%s`' % (
432 432 perm.permission_name, apply_to_children, user.username,
433 433 repo_group.name
434 434 ),
435 435 'success': True
436 436 }
437 437 except Exception:
438 438 log.exception("Exception occurred while trying to grant "
439 439 "user permissions to repo group")
440 440 raise JSONRPCError(
441 441 'failed to edit permission for user: '
442 442 '`%s` in repo group: `%s`' % (userid, repo_group.name))
443 443
444 444
445 445 @jsonrpc_method()
446 446 def revoke_user_permission_from_repo_group(
447 447 request, apiuser, repogroupid, userid,
448 448 apply_to_children=Optional('none')):
449 449 """
450 450 Revoke permission for a user in a given repository group.
451 451
452 452 This command can only be run using an |authtoken| with admin
453 453 permissions on the |repo| group.
454 454
455 455 :param apiuser: This is filled automatically from the |authtoken|.
456 456 :type apiuser: AuthUser
457 457 :param repogroupid: Set the name or ID of the repository group.
458 458 :type repogroupid: str or int
459 459 :param userid: Set the user name to revoke.
460 460 :type userid: str
461 461 :param apply_to_children: 'none', 'repos', 'groups', 'all'
462 462 :type apply_to_children: str
463 463
464 464 Example output:
465 465
466 466 .. code-block:: bash
467 467
468 468 id : <id_given_in_input>
469 469 result: {
470 470 "msg" : "Revoked perm (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`",
471 471 "success": true
472 472 }
473 473 error: null
474 474
475 475 Example error output:
476 476
477 477 .. code-block:: bash
478 478
479 479 id : <id_given_in_input>
480 480 result : null
481 481 error : {
482 482 "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`"
483 483 }
484 484
485 485 """
486 486
487 487 repo_group = get_repo_group_or_error(repogroupid)
488 488
489 489 if not has_superadmin_permission(apiuser):
490 490 # check if we have admin permission for this repo group !
491 491 _perms = ('group.admin',)
492 492 if not HasRepoGroupPermissionAnyApi(*_perms)(
493 493 user=apiuser, group_name=repo_group.group_name):
494 494 raise JSONRPCError(
495 495 'repository group `%s` does not exist' % (repogroupid,))
496 496
497 497 user = get_user_or_error(userid)
498 498 apply_to_children = Optional.extract(apply_to_children)
499 499
500 500 perm_deletions = [[user.user_id, None, "user"]]
501 501 try:
502 502 RepoGroupModel().update_permissions(repo_group=repo_group,
503 503 perm_deletions=perm_deletions,
504 504 recursive=apply_to_children,
505 505 cur_user=apiuser)
506 506 Session().commit()
507 507 return {
508 508 'msg': 'Revoked perm (recursive:%s) for user: '
509 509 '`%s` in repo group: `%s`' % (
510 510 apply_to_children, user.username, repo_group.name
511 511 ),
512 512 'success': True
513 513 }
514 514 except Exception:
515 515 log.exception("Exception occurred while trying revoke user "
516 516 "permission from repo group")
517 517 raise JSONRPCError(
518 518 'failed to edit permission for user: '
519 519 '`%s` in repo group: `%s`' % (userid, repo_group.name))
520 520
521 521
522 522 @jsonrpc_method()
523 523 def grant_user_group_permission_to_repo_group(
524 524 request, apiuser, repogroupid, usergroupid, perm,
525 525 apply_to_children=Optional('none'), ):
526 526 """
527 527 Grant permission for a user group on given repository group, or update
528 528 existing permissions if found.
529 529
530 530 This command can only be run using an |authtoken| with admin
531 531 permissions on the |repo| group.
532 532
533 533 :param apiuser: This is filled automatically from the |authtoken|.
534 534 :type apiuser: AuthUser
535 535 :param repogroupid: Set the name or id of repository group
536 536 :type repogroupid: str or int
537 537 :param usergroupid: id of usergroup
538 538 :type usergroupid: str or int
539 539 :param perm: (group.(none|read|write|admin))
540 540 :type perm: str
541 541 :param apply_to_children: 'none', 'repos', 'groups', 'all'
542 542 :type apply_to_children: str
543 543
544 544 Example output:
545 545
546 546 .. code-block:: bash
547 547
548 548 id : <id_given_in_input>
549 549 result : {
550 550 "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`",
551 551 "success": true
552 552
553 553 }
554 554 error : null
555 555
556 556 Example error output:
557 557
558 558 .. code-block:: bash
559 559
560 560 id : <id_given_in_input>
561 561 result : null
562 562 error : {
563 563 "failed to edit permission for user group: `<usergroup>` in repo group: `<repo_group_name>`"
564 564 }
565 565
566 566 """
567 567
568 568 repo_group = get_repo_group_or_error(repogroupid)
569 569 perm = get_perm_or_error(perm, prefix='group.')
570 570 user_group = get_user_group_or_error(usergroupid)
571 571 if not has_superadmin_permission(apiuser):
572 572 # check if we have admin permission for this repo group !
573 573 _perms = ('group.admin',)
574 574 if not HasRepoGroupPermissionAnyApi(*_perms)(
575 575 user=apiuser, group_name=repo_group.group_name):
576 576 raise JSONRPCError(
577 577 'repository group `%s` does not exist' % (repogroupid,))
578 578
579 579 # check if we have at least read permission for this user group !
580 580 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
581 581 if not HasUserGroupPermissionAnyApi(*_perms)(
582 582 user=apiuser, user_group_name=user_group.users_group_name):
583 583 raise JSONRPCError(
584 584 'user group `%s` does not exist' % (usergroupid,))
585 585
586 586 apply_to_children = Optional.extract(apply_to_children)
587 587
588 588 perm_additions = [[user_group.users_group_id, perm, "user_group"]]
589 589 try:
590 590 RepoGroupModel().update_permissions(repo_group=repo_group,
591 591 perm_additions=perm_additions,
592 592 recursive=apply_to_children,
593 593 cur_user=apiuser)
594 594 Session().commit()
595 595 return {
596 596 'msg': 'Granted perm: `%s` (recursive:%s) '
597 597 'for user group: `%s` in repo group: `%s`' % (
598 598 perm.permission_name, apply_to_children,
599 599 user_group.users_group_name, repo_group.name
600 600 ),
601 601 'success': True
602 602 }
603 603 except Exception:
604 604 log.exception("Exception occurred while trying to grant user "
605 605 "group permissions to repo group")
606 606 raise JSONRPCError(
607 607 'failed to edit permission for user group: `%s` in '
608 608 'repo group: `%s`' % (
609 609 usergroupid, repo_group.name
610 610 )
611 611 )
612 612
613 613
614 614 @jsonrpc_method()
615 615 def revoke_user_group_permission_from_repo_group(
616 616 request, apiuser, repogroupid, usergroupid,
617 617 apply_to_children=Optional('none')):
618 618 """
619 619 Revoke permission for user group on given repository.
620 620
621 621 This command can only be run using an |authtoken| with admin
622 622 permissions on the |repo| group.
623 623
624 624 :param apiuser: This is filled automatically from the |authtoken|.
625 625 :type apiuser: AuthUser
626 626 :param repogroupid: name or id of repository group
627 627 :type repogroupid: str or int
628 628 :param usergroupid:
629 629 :param apply_to_children: 'none', 'repos', 'groups', 'all'
630 630 :type apply_to_children: str
631 631
632 632 Example output:
633 633
634 634 .. code-block:: bash
635 635
636 636 id : <id_given_in_input>
637 637 result: {
638 638 "msg" : "Revoked perm (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`",
639 639 "success": true
640 640 }
641 641 error: null
642 642
643 643 Example error output:
644 644
645 645 .. code-block:: bash
646 646
647 647 id : <id_given_in_input>
648 648 result : null
649 649 error : {
650 650 "failed to edit permission for user group: `<usergroup>` in repo group: `<repo_group_name>`"
651 651 }
652 652
653 653
654 654 """
655 655
656 656 repo_group = get_repo_group_or_error(repogroupid)
657 657 user_group = get_user_group_or_error(usergroupid)
658 658 if not has_superadmin_permission(apiuser):
659 659 # check if we have admin permission for this repo group !
660 660 _perms = ('group.admin',)
661 661 if not HasRepoGroupPermissionAnyApi(*_perms)(
662 662 user=apiuser, group_name=repo_group.group_name):
663 663 raise JSONRPCError(
664 664 'repository group `%s` does not exist' % (repogroupid,))
665 665
666 666 # check if we have at least read permission for this user group !
667 667 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
668 668 if not HasUserGroupPermissionAnyApi(*_perms)(
669 669 user=apiuser, user_group_name=user_group.users_group_name):
670 670 raise JSONRPCError(
671 671 'user group `%s` does not exist' % (usergroupid,))
672 672
673 673 apply_to_children = Optional.extract(apply_to_children)
674 674
675 675 perm_deletions = [[user_group.users_group_id, None, "user_group"]]
676 676 try:
677 677 RepoGroupModel().update_permissions(repo_group=repo_group,
678 678 perm_deletions=perm_deletions,
679 679 recursive=apply_to_children,
680 680 cur_user=apiuser)
681 681 Session().commit()
682 682 return {
683 683 'msg': 'Revoked perm (recursive:%s) for user group: '
684 684 '`%s` in repo group: `%s`' % (
685 685 apply_to_children, user_group.users_group_name,
686 686 repo_group.name
687 687 ),
688 688 'success': True
689 689 }
690 690 except Exception:
691 691 log.exception("Exception occurred while trying revoke user group "
692 692 "permissions from repo group")
693 693 raise JSONRPCError(
694 694 'failed to edit permission for user group: '
695 695 '`%s` in repo group: `%s`' % (
696 696 user_group.users_group_name, repo_group.name
697 697 )
698 698 )
699 699
@@ -1,339 +1,367 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2013-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 gist controller for RhodeCode
24 24 """
25 25
26 26 import time
27 27 import logging
28 import traceback
28
29 29 import formencode
30 import peppercorn
30 31 from formencode import htmlfill
31 32
32 33 from pylons import request, response, tmpl_context as c, url
33 34 from pylons.controllers.util import abort, redirect
34 35 from pylons.i18n.translation import _
36 from webob.exc import HTTPNotFound, HTTPForbidden
37 from sqlalchemy.sql.expression import or_
35 38
36 from rhodecode.model.forms import GistForm
39
37 40 from rhodecode.model.gist import GistModel
38 41 from rhodecode.model.meta import Session
39 42 from rhodecode.model.db import Gist, User
40 43 from rhodecode.lib import auth
41 44 from rhodecode.lib import helpers as h
42 45 from rhodecode.lib.base import BaseController, render
43 46 from rhodecode.lib.auth import LoginRequired, NotAnonymous
44 47 from rhodecode.lib.utils import jsonify
45 48 from rhodecode.lib.utils2 import safe_str, safe_int, time_to_datetime
46 49 from rhodecode.lib.ext_json import json
47 from webob.exc import HTTPNotFound, HTTPForbidden
48 from sqlalchemy.sql.expression import or_
49 50 from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError
51 from rhodecode.model import validation_schema
52 from rhodecode.model.validation_schema.schemas import gist_schema
53
50 54
51 55 log = logging.getLogger(__name__)
52 56
53 57
54 58 class GistsController(BaseController):
55 59 """REST Controller styled on the Atom Publishing Protocol"""
56 60
57 61 def __load_defaults(self, extra_values=None):
58 62 c.lifetime_values = [
59 (str(-1), _('forever')),
60 (str(5), _('5 minutes')),
61 (str(60), _('1 hour')),
62 (str(60 * 24), _('1 day')),
63 (str(60 * 24 * 30), _('1 month')),
63 (-1, _('forever')),
64 (5, _('5 minutes')),
65 (60, _('1 hour')),
66 (60 * 24, _('1 day')),
67 (60 * 24 * 30, _('1 month')),
64 68 ]
65 69 if extra_values:
66 70 c.lifetime_values.append(extra_values)
67 71 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
68 72 c.acl_options = [
69 73 (Gist.ACL_LEVEL_PRIVATE, _("Requires registered account")),
70 74 (Gist.ACL_LEVEL_PUBLIC, _("Can be accessed by anonymous users"))
71 75 ]
72 76
73 77 @LoginRequired()
74 78 def index(self):
75 79 """GET /admin/gists: All items in the collection"""
76 80 # url('gists')
77 81 not_default_user = c.rhodecode_user.username != User.DEFAULT_USER
78 82 c.show_private = request.GET.get('private') and not_default_user
79 83 c.show_public = request.GET.get('public') and not_default_user
80 84 c.show_all = request.GET.get('all') and c.rhodecode_user.admin
81 85
82 86 gists = _gists = Gist().query()\
83 87 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
84 88 .order_by(Gist.created_on.desc())
85 89
86 90 c.active = 'public'
87 91 # MY private
88 92 if c.show_private and not c.show_public:
89 93 gists = _gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
90 94 .filter(Gist.gist_owner == c.rhodecode_user.user_id)
91 95 c.active = 'my_private'
92 96 # MY public
93 97 elif c.show_public and not c.show_private:
94 98 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\
95 99 .filter(Gist.gist_owner == c.rhodecode_user.user_id)
96 100 c.active = 'my_public'
97 101 # MY public+private
98 102 elif c.show_private and c.show_public:
99 103 gists = _gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
100 104 Gist.gist_type == Gist.GIST_PRIVATE))\
101 105 .filter(Gist.gist_owner == c.rhodecode_user.user_id)
102 106 c.active = 'my_all'
103 107 # Show all by super-admin
104 108 elif c.show_all:
105 109 c.active = 'all'
106 110 gists = _gists
107 111
108 112 # default show ALL public gists
109 113 if not c.show_public and not c.show_private and not c.show_all:
110 114 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
111 115 c.active = 'public'
112 116
113 117 from rhodecode.lib.utils import PartialRenderer
114 118 _render = PartialRenderer('data_table/_dt_elements.html')
115 119
116 120 data = []
117 121
118 122 for gist in gists:
119 123 data.append({
120 124 'created_on': _render('gist_created', gist.created_on),
121 125 'created_on_raw': gist.created_on,
122 126 'type': _render('gist_type', gist.gist_type),
123 127 'access_id': _render('gist_access_id', gist.gist_access_id, gist.owner.full_contact),
124 128 'author': _render('gist_author', gist.owner.full_contact, gist.created_on, gist.gist_expires),
125 129 'author_raw': h.escape(gist.owner.full_contact),
126 130 'expires': _render('gist_expires', gist.gist_expires),
127 131 'description': _render('gist_description', gist.gist_description)
128 132 })
129 133 c.data = json.dumps(data)
130 134 return render('admin/gists/index.html')
131 135
132 136 @LoginRequired()
133 137 @NotAnonymous()
134 138 @auth.CSRFRequired()
135 139 def create(self):
136 140 """POST /admin/gists: Create a new item"""
137 141 # url('gists')
138 142 self.__load_defaults()
139 gist_form = GistForm([x[0] for x in c.lifetime_values],
140 [x[0] for x in c.acl_options])()
143
144 data = dict(request.POST)
145 data['filename'] = data.get('filename') or Gist.DEFAULT_FILENAME
146 data['nodes'] = [{
147 'filename': data['filename'],
148 'content': data.get('content'),
149 'mimetype': data.get('mimetype') # None is autodetect
150 }]
151
152 data['gist_type'] = (
153 Gist.GIST_PUBLIC if data.get('public') else Gist.GIST_PRIVATE)
154 data['gist_acl_level'] = (
155 data.get('gist_acl_level') or Gist.ACL_LEVEL_PRIVATE)
156
157 schema = gist_schema.GistSchema().bind(
158 lifetime_options=[x[0] for x in c.lifetime_values])
159
141 160 try:
142 form_result = gist_form.to_python(dict(request.POST))
143 # TODO: multiple files support, from the form
144 filename = form_result['filename'] or Gist.DEFAULT_FILENAME
145 nodes = {
146 filename: {
147 'content': form_result['content'],
148 'lexer': form_result['mimetype'] # None is autodetect
149 }
150 }
151 _public = form_result['public']
152 gist_type = Gist.GIST_PUBLIC if _public else Gist.GIST_PRIVATE
153 gist_acl_level = form_result.get(
154 'acl_level', Gist.ACL_LEVEL_PRIVATE)
161
162 schema_data = schema.deserialize(data)
163 # convert to safer format with just KEYs so we sure no duplicates
164 schema_data['nodes'] = gist_schema.sequence_to_nodes(
165 schema_data['nodes'])
166
155 167 gist = GistModel().create(
156 description=form_result['description'],
168 gist_id=schema_data['gistid'], # custom access id not real ID
169 description=schema_data['description'],
157 170 owner=c.rhodecode_user.user_id,
158 gist_mapping=nodes,
159 gist_type=gist_type,
160 lifetime=form_result['lifetime'],
161 gist_id=form_result['gistid'],
162 gist_acl_level=gist_acl_level
171 gist_mapping=schema_data['nodes'],
172 gist_type=schema_data['gist_type'],
173 lifetime=schema_data['lifetime'],
174 gist_acl_level=schema_data['gist_acl_level']
163 175 )
164 176 Session().commit()
165 177 new_gist_id = gist.gist_access_id
166 except formencode.Invalid as errors:
167 defaults = errors.value
178 except validation_schema.Invalid as errors:
179 defaults = data
180 errors = errors.asdict()
181
182 if 'nodes.0.content' in errors:
183 errors['content'] = errors['nodes.0.content']
184 del errors['nodes.0.content']
185 if 'nodes.0.filename' in errors:
186 errors['filename'] = errors['nodes.0.filename']
187 del errors['nodes.0.filename']
168 188
169 189 return formencode.htmlfill.render(
170 190 render('admin/gists/new.html'),
171 191 defaults=defaults,
172 errors=errors.error_dict or {},
192 errors=errors,
173 193 prefix_error=False,
174 194 encoding="UTF-8",
175 195 force_defaults=False
176 196 )
177 197
178 198 except Exception:
179 199 log.exception("Exception while trying to create a gist")
180 200 h.flash(_('Error occurred during gist creation'), category='error')
181 201 return redirect(url('new_gist'))
182 202 return redirect(url('gist', gist_id=new_gist_id))
183 203
184 204 @LoginRequired()
185 205 @NotAnonymous()
186 206 def new(self, format='html'):
187 207 """GET /admin/gists/new: Form to create a new item"""
188 208 # url('new_gist')
189 209 self.__load_defaults()
190 210 return render('admin/gists/new.html')
191 211
192 212 @LoginRequired()
193 213 @NotAnonymous()
194 214 @auth.CSRFRequired()
195 215 def delete(self, gist_id):
196 216 """DELETE /admin/gists/gist_id: Delete an existing item"""
197 217 # Forms posted to this method should contain a hidden field:
198 218 # <input type="hidden" name="_method" value="DELETE" />
199 219 # Or using helpers:
200 220 # h.form(url('gist', gist_id=ID),
201 221 # method='delete')
202 222 # url('gist', gist_id=ID)
203 223 c.gist = Gist.get_or_404(gist_id)
204 224
205 225 owner = c.gist.gist_owner == c.rhodecode_user.user_id
206 226 if not (h.HasPermissionAny('hg.admin')() or owner):
207 227 raise HTTPForbidden()
208 228
209 229 GistModel().delete(c.gist)
210 230 Session().commit()
211 231 h.flash(_('Deleted gist %s') % c.gist.gist_access_id, category='success')
212 232
213 233 return redirect(url('gists'))
214 234
215 235 def _add_gist_to_context(self, gist_id):
216 236 c.gist = Gist.get_or_404(gist_id)
217 237
218 238 # Check if this gist is expired
219 239 if c.gist.gist_expires != -1:
220 240 if time.time() > c.gist.gist_expires:
221 241 log.error(
222 242 'Gist expired at %s', time_to_datetime(c.gist.gist_expires))
223 243 raise HTTPNotFound()
224 244
225 245 # check if this gist requires a login
226 246 is_default_user = c.rhodecode_user.username == User.DEFAULT_USER
227 247 if c.gist.acl_level == Gist.ACL_LEVEL_PRIVATE and is_default_user:
228 248 log.error("Anonymous user %s tried to access protected gist `%s`",
229 249 c.rhodecode_user, gist_id)
230 250 raise HTTPNotFound()
231 251
232 252 @LoginRequired()
233 253 def show(self, gist_id, revision='tip', format='html', f_path=None):
234 254 """GET /admin/gists/gist_id: Show a specific item"""
235 255 # url('gist', gist_id=ID)
236 256 self._add_gist_to_context(gist_id)
237 257 c.render = not request.GET.get('no-render', False)
238 258
239 259 try:
240 260 c.file_last_commit, c.files = GistModel().get_gist_files(
241 261 gist_id, revision=revision)
242 262 except VCSError:
243 263 log.exception("Exception in gist show")
244 264 raise HTTPNotFound()
245 265 if format == 'raw':
246 content = '\n\n'.join([f.content for f in c.files if (f_path is None or f.path == f_path)])
266 content = '\n\n'.join([f.content for f in c.files
267 if (f_path is None or f.path == f_path)])
247 268 response.content_type = 'text/plain'
248 269 return content
249 270 return render('admin/gists/show.html')
250 271
251 272 @LoginRequired()
252 273 @NotAnonymous()
253 274 @auth.CSRFRequired()
254 275 def edit(self, gist_id):
276 self.__load_defaults()
255 277 self._add_gist_to_context(gist_id)
256 278
257 279 owner = c.gist.gist_owner == c.rhodecode_user.user_id
258 280 if not (h.HasPermissionAny('hg.admin')() or owner):
259 281 raise HTTPForbidden()
260 282
261 rpost = request.POST
262 nodes = {}
263 _file_data = zip(rpost.getall('org_files'), rpost.getall('files'),
264 rpost.getall('mimetypes'), rpost.getall('contents'))
265 for org_filename, filename, mimetype, content in _file_data:
266 nodes[org_filename] = {
267 'org_filename': org_filename,
268 'filename': filename,
269 'content': content,
270 'lexer': mimetype,
271 }
283 data = peppercorn.parse(request.POST.items())
284
285 schema = gist_schema.GistSchema()
286 schema = schema.bind(
287 # '0' is special value to leave lifetime untouched
288 lifetime_options=[x[0] for x in c.lifetime_values] + [0],
289 )
290
272 291 try:
292 schema_data = schema.deserialize(data)
293 # convert to safer format with just KEYs so we sure no duplicates
294 schema_data['nodes'] = gist_schema.sequence_to_nodes(
295 schema_data['nodes'])
296
273 297 GistModel().update(
274 298 gist=c.gist,
275 description=rpost['description'],
299 description=schema_data['description'],
276 300 owner=c.gist.owner,
277 gist_mapping=nodes,
278 gist_type=c.gist.gist_type,
279 lifetime=rpost['lifetime'],
280 gist_acl_level=rpost['acl_level']
301 gist_mapping=schema_data['nodes'],
302 gist_type=schema_data['gist_type'],
303 lifetime=schema_data['lifetime'],
304 gist_acl_level=schema_data['gist_acl_level']
281 305 )
282 306
283 307 Session().commit()
284 308 h.flash(_('Successfully updated gist content'), category='success')
285 309 except NodeNotChangedError:
286 310 # raised if nothing was changed in repo itself. We anyway then
287 311 # store only DB stuff for gist
288 312 Session().commit()
289 313 h.flash(_('Successfully updated gist data'), category='success')
314 except validation_schema.Invalid as errors:
315 errors = errors.asdict()
316 h.flash(_('Error occurred during update of gist {}: {}').format(
317 gist_id, errors), category='error')
290 318 except Exception:
291 319 log.exception("Exception in gist edit")
292 320 h.flash(_('Error occurred during update of gist %s') % gist_id,
293 321 category='error')
294 322
295 323 return redirect(url('gist', gist_id=gist_id))
296 324
297 325 @LoginRequired()
298 326 @NotAnonymous()
299 327 def edit_form(self, gist_id, format='html'):
300 328 """GET /admin/gists/gist_id/edit: Form to edit an existing item"""
301 329 # url('edit_gist', gist_id=ID)
302 330 self._add_gist_to_context(gist_id)
303 331
304 332 owner = c.gist.gist_owner == c.rhodecode_user.user_id
305 333 if not (h.HasPermissionAny('hg.admin')() or owner):
306 334 raise HTTPForbidden()
307 335
308 336 try:
309 337 c.file_last_commit, c.files = GistModel().get_gist_files(gist_id)
310 338 except VCSError:
311 339 log.exception("Exception in gist edit")
312 340 raise HTTPNotFound()
313 341
314 342 if c.gist.gist_expires == -1:
315 343 expiry = _('never')
316 344 else:
317 345 # this cannot use timeago, since it's used in select2 as a value
318 346 expiry = h.age(h.time_to_datetime(c.gist.gist_expires))
319 347 self.__load_defaults(
320 extra_values=('0', _('%(expiry)s - current value') % {'expiry': expiry}))
348 extra_values=(0, _('%(expiry)s - current value') % {'expiry': expiry}))
321 349 return render('admin/gists/edit.html')
322 350
323 351 @LoginRequired()
324 352 @NotAnonymous()
325 353 @jsonify
326 354 def check_revision(self, gist_id):
327 355 c.gist = Gist.get_or_404(gist_id)
328 356 last_rev = c.gist.scm_instance().get_commit()
329 357 success = True
330 358 revision = request.GET.get('revision')
331 359
332 360 ##TODO: maybe move this to model ?
333 361 if revision != last_rev.raw_id:
334 362 log.error('Last revision %s is different then submitted %s'
335 363 % (revision, last_rev))
336 364 # our gist has newer version than we
337 365 success = False
338 366
339 367 return {'success': success}
@@ -1,111 +1,111 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Search controller for RhodeCode
23 23 """
24 24
25 25 import logging
26 26 import urllib
27 27
28 28 from pylons import request, config, tmpl_context as c
29 29
30 30 from webhelpers.util import update_params
31 31
32 32 from rhodecode.lib.auth import LoginRequired, AuthUser
33 33 from rhodecode.lib.base import BaseRepoController, render
34 34 from rhodecode.lib.helpers import Page
35 35 from rhodecode.lib.utils2 import safe_str, safe_int
36 36 from rhodecode.lib.index import searcher_from_config
37 37 from rhodecode.model import validation_schema
38 from rhodecode.model.validation_schema.schemas import search_schema
38 39
39 40 log = logging.getLogger(__name__)
40 41
41 42
42 43 class SearchController(BaseRepoController):
43 44
44 45 @LoginRequired()
45 46 def index(self, repo_name=None):
46 47
47 48 searcher = searcher_from_config(config)
48 49 formatted_results = []
49 50 execution_time = ''
50 51
51 schema = validation_schema.SearchParamsSchema()
52 schema = search_schema.SearchParamsSchema()
52 53
53 54 search_params = {}
54 55 errors = []
55 56 try:
56 57 search_params = schema.deserialize(
57 58 dict(search_query=request.GET.get('q'),
58 59 search_type=request.GET.get('type'),
59 60 search_sort=request.GET.get('sort'),
60 61 page_limit=request.GET.get('page_limit'),
61 62 requested_page=request.GET.get('page'))
62 63 )
63 64 except validation_schema.Invalid as e:
64 65 errors = e.children
65 66
66 67 def url_generator(**kw):
67 68 q = urllib.quote(safe_str(search_query))
68 69 return update_params(
69 70 "?q=%s&type=%s" % (q, safe_str(search_type)), **kw)
70 71
71 72 search_query = search_params.get('search_query')
72 73 search_type = search_params.get('search_type')
73 74 search_sort = search_params.get('search_sort')
74 75 if search_params.get('search_query'):
75 76 page_limit = search_params['page_limit']
76 77 requested_page = search_params['requested_page']
77 78
78
79 79 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
80 80 ip_addr=self.ip_addr)
81 81
82 82 try:
83 83 search_result = searcher.search(
84 84 search_query, search_type, c.perm_user, repo_name,
85 85 requested_page, page_limit, search_sort)
86 86
87 87 formatted_results = Page(
88 88 search_result['results'], page=requested_page,
89 89 item_count=search_result['count'],
90 90 items_per_page=page_limit, url=url_generator)
91 91 finally:
92 92 searcher.cleanup()
93 93
94 94 if not search_result['error']:
95 95 execution_time = '%s results (%.3f seconds)' % (
96 96 search_result['count'],
97 97 search_result['runtime'])
98 98 elif not errors:
99 99 node = schema['search_query']
100 100 errors = [
101 101 validation_schema.Invalid(node, search_result['error'])]
102 102
103 103 c.sort = search_sort
104 104 c.url_generator = url_generator
105 105 c.errors = errors
106 106 c.formatted_results = formatted_results
107 107 c.runtime = execution_time
108 108 c.cur_query = search_query
109 109 c.search_type = search_type
110 110 # Return a rendered template
111 111 return render('/search/search.html')
@@ -1,580 +1,563 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 this is forms validation classes
23 23 http://formencode.org/module-formencode.validators.html
24 24 for list off all availible validators
25 25
26 26 we can create our own validators
27 27
28 28 The table below outlines the options which can be used in a schema in addition to the validators themselves
29 29 pre_validators [] These validators will be applied before the schema
30 30 chained_validators [] These validators will be applied after the schema
31 31 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
32 32 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
33 33 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
34 34 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
35 35
36 36
37 37 <name> = formencode.validators.<name of validator>
38 38 <name> must equal form name
39 39 list=[1,2,3,4,5]
40 40 for SELECT use formencode.All(OneOf(list), Int())
41 41
42 42 """
43 43
44 44 import deform
45 45 import logging
46 46 import formencode
47 47
48 48 from pkg_resources import resource_filename
49 49 from formencode import All, Pipe
50 50
51 51 from pylons.i18n.translation import _
52 52
53 53 from rhodecode import BACKENDS
54 54 from rhodecode.lib import helpers
55 55 from rhodecode.model import validators as v
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 deform_templates = resource_filename('deform', 'templates')
61 61 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
62 62 search_path = (rhodecode_templates, deform_templates)
63 63
64 64
65 65 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
66 66 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
67 67 def __call__(self, template_name, **kw):
68 68 kw['h'] = helpers
69 69 return self.load(template_name)(**kw)
70 70
71 71
72 72 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
73 73 deform.Form.set_default_renderer(form_renderer)
74 74
75 75
76 76 def LoginForm():
77 77 class _LoginForm(formencode.Schema):
78 78 allow_extra_fields = True
79 79 filter_extra_fields = True
80 80 username = v.UnicodeString(
81 81 strip=True,
82 82 min=1,
83 83 not_empty=True,
84 84 messages={
85 85 'empty': _(u'Please enter a login'),
86 86 'tooShort': _(u'Enter a value %(min)i characters long or more')
87 87 }
88 88 )
89 89
90 90 password = v.UnicodeString(
91 91 strip=False,
92 92 min=3,
93 93 not_empty=True,
94 94 messages={
95 95 'empty': _(u'Please enter a password'),
96 96 'tooShort': _(u'Enter %(min)i characters or more')}
97 97 )
98 98
99 99 remember = v.StringBoolean(if_missing=False)
100 100
101 101 chained_validators = [v.ValidAuth()]
102 102 return _LoginForm
103 103
104 104
105 105 def PasswordChangeForm(username):
106 106 class _PasswordChangeForm(formencode.Schema):
107 107 allow_extra_fields = True
108 108 filter_extra_fields = True
109 109
110 110 current_password = v.ValidOldPassword(username)(not_empty=True)
111 111 new_password = All(v.ValidPassword(), v.UnicodeString(strip=False, min=6))
112 112 new_password_confirmation = All(v.ValidPassword(), v.UnicodeString(strip=False, min=6))
113 113
114 114 chained_validators = [v.ValidPasswordsMatch('new_password',
115 115 'new_password_confirmation')]
116 116 return _PasswordChangeForm
117 117
118 118
119 119 def UserForm(edit=False, available_languages=[], old_data={}):
120 120 class _UserForm(formencode.Schema):
121 121 allow_extra_fields = True
122 122 filter_extra_fields = True
123 123 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
124 124 v.ValidUsername(edit, old_data))
125 125 if edit:
126 126 new_password = All(
127 127 v.ValidPassword(),
128 128 v.UnicodeString(strip=False, min=6, not_empty=False)
129 129 )
130 130 password_confirmation = All(
131 131 v.ValidPassword(),
132 132 v.UnicodeString(strip=False, min=6, not_empty=False),
133 133 )
134 134 admin = v.StringBoolean(if_missing=False)
135 135 else:
136 136 password = All(
137 137 v.ValidPassword(),
138 138 v.UnicodeString(strip=False, min=6, not_empty=True)
139 139 )
140 140 password_confirmation = All(
141 141 v.ValidPassword(),
142 142 v.UnicodeString(strip=False, min=6, not_empty=False)
143 143 )
144 144
145 145 password_change = v.StringBoolean(if_missing=False)
146 146 create_repo_group = v.StringBoolean(if_missing=False)
147 147
148 148 active = v.StringBoolean(if_missing=False)
149 149 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
150 150 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
151 151 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
152 152 extern_name = v.UnicodeString(strip=True)
153 153 extern_type = v.UnicodeString(strip=True)
154 154 language = v.OneOf(available_languages, hideList=False,
155 155 testValueList=True, if_missing=None)
156 156 chained_validators = [v.ValidPasswordsMatch()]
157 157 return _UserForm
158 158
159 159
160 160 def UserGroupForm(edit=False, old_data=None, available_members=None,
161 161 allow_disabled=False):
162 162 old_data = old_data or {}
163 163 available_members = available_members or []
164 164
165 165 class _UserGroupForm(formencode.Schema):
166 166 allow_extra_fields = True
167 167 filter_extra_fields = True
168 168
169 169 users_group_name = All(
170 170 v.UnicodeString(strip=True, min=1, not_empty=True),
171 171 v.ValidUserGroup(edit, old_data)
172 172 )
173 173 user_group_description = v.UnicodeString(strip=True, min=1,
174 174 not_empty=False)
175 175
176 176 users_group_active = v.StringBoolean(if_missing=False)
177 177
178 178 if edit:
179 179 users_group_members = v.OneOf(
180 180 available_members, hideList=False, testValueList=True,
181 181 if_missing=None, not_empty=False
182 182 )
183 183 # this is user group owner
184 184 user = All(
185 185 v.UnicodeString(not_empty=True),
186 186 v.ValidRepoUser(allow_disabled))
187 187 return _UserGroupForm
188 188
189 189
190 190 def RepoGroupForm(edit=False, old_data=None, available_groups=None,
191 191 can_create_in_root=False, allow_disabled=False):
192 192 old_data = old_data or {}
193 193 available_groups = available_groups or []
194 194
195 195 class _RepoGroupForm(formencode.Schema):
196 196 allow_extra_fields = True
197 197 filter_extra_fields = False
198 198
199 199 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
200 200 v.SlugifyName(),)
201 201 group_description = v.UnicodeString(strip=True, min=1,
202 202 not_empty=False)
203 203 group_copy_permissions = v.StringBoolean(if_missing=False)
204 204
205 205 group_parent_id = v.OneOf(available_groups, hideList=False,
206 206 testValueList=True, not_empty=True)
207 207 enable_locking = v.StringBoolean(if_missing=False)
208 208 chained_validators = [
209 209 v.ValidRepoGroup(edit, old_data, can_create_in_root)]
210 210
211 211 if edit:
212 212 # this is repo group owner
213 213 user = All(
214 214 v.UnicodeString(not_empty=True),
215 215 v.ValidRepoUser(allow_disabled))
216 216
217 217 return _RepoGroupForm
218 218
219 219
220 220 def RegisterForm(edit=False, old_data={}):
221 221 class _RegisterForm(formencode.Schema):
222 222 allow_extra_fields = True
223 223 filter_extra_fields = True
224 224 username = All(
225 225 v.ValidUsername(edit, old_data),
226 226 v.UnicodeString(strip=True, min=1, not_empty=True)
227 227 )
228 228 password = All(
229 229 v.ValidPassword(),
230 230 v.UnicodeString(strip=False, min=6, not_empty=True)
231 231 )
232 232 password_confirmation = All(
233 233 v.ValidPassword(),
234 234 v.UnicodeString(strip=False, min=6, not_empty=True)
235 235 )
236 236 active = v.StringBoolean(if_missing=False)
237 237 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
238 238 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
239 239 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
240 240
241 241 chained_validators = [v.ValidPasswordsMatch()]
242 242
243 243 return _RegisterForm
244 244
245 245
246 246 def PasswordResetForm():
247 247 class _PasswordResetForm(formencode.Schema):
248 248 allow_extra_fields = True
249 249 filter_extra_fields = True
250 250 email = All(v.ValidSystemEmail(), v.Email(not_empty=True))
251 251 return _PasswordResetForm
252 252
253 253
254 254 def RepoForm(edit=False, old_data=None, repo_groups=None, landing_revs=None,
255 255 allow_disabled=False):
256 256 old_data = old_data or {}
257 257 repo_groups = repo_groups or []
258 258 landing_revs = landing_revs or []
259 259 supported_backends = BACKENDS.keys()
260 260
261 261 class _RepoForm(formencode.Schema):
262 262 allow_extra_fields = True
263 263 filter_extra_fields = False
264 264 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
265 265 v.SlugifyName())
266 266 repo_group = All(v.CanWriteGroup(old_data),
267 267 v.OneOf(repo_groups, hideList=True))
268 268 repo_type = v.OneOf(supported_backends, required=False,
269 269 if_missing=old_data.get('repo_type'))
270 270 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
271 271 repo_private = v.StringBoolean(if_missing=False)
272 272 repo_landing_rev = v.OneOf(landing_revs, hideList=True)
273 273 repo_copy_permissions = v.StringBoolean(if_missing=False)
274 274 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
275 275
276 276 repo_enable_statistics = v.StringBoolean(if_missing=False)
277 277 repo_enable_downloads = v.StringBoolean(if_missing=False)
278 278 repo_enable_locking = v.StringBoolean(if_missing=False)
279 279
280 280 if edit:
281 281 # this is repo owner
282 282 user = All(
283 283 v.UnicodeString(not_empty=True),
284 284 v.ValidRepoUser(allow_disabled))
285 285 clone_uri_change = v.UnicodeString(
286 286 not_empty=False, if_missing=v.Missing)
287 287
288 288 chained_validators = [v.ValidCloneUri(),
289 289 v.ValidRepoName(edit, old_data)]
290 290 return _RepoForm
291 291
292 292
293 293 def RepoPermsForm():
294 294 class _RepoPermsForm(formencode.Schema):
295 295 allow_extra_fields = True
296 296 filter_extra_fields = False
297 297 chained_validators = [v.ValidPerms(type_='repo')]
298 298 return _RepoPermsForm
299 299
300 300
301 301 def RepoGroupPermsForm(valid_recursive_choices):
302 302 class _RepoGroupPermsForm(formencode.Schema):
303 303 allow_extra_fields = True
304 304 filter_extra_fields = False
305 305 recursive = v.OneOf(valid_recursive_choices)
306 306 chained_validators = [v.ValidPerms(type_='repo_group')]
307 307 return _RepoGroupPermsForm
308 308
309 309
310 310 def UserGroupPermsForm():
311 311 class _UserPermsForm(formencode.Schema):
312 312 allow_extra_fields = True
313 313 filter_extra_fields = False
314 314 chained_validators = [v.ValidPerms(type_='user_group')]
315 315 return _UserPermsForm
316 316
317 317
318 318 def RepoFieldForm():
319 319 class _RepoFieldForm(formencode.Schema):
320 320 filter_extra_fields = True
321 321 allow_extra_fields = True
322 322
323 323 new_field_key = All(v.FieldKey(),
324 324 v.UnicodeString(strip=True, min=3, not_empty=True))
325 325 new_field_value = v.UnicodeString(not_empty=False, if_missing=u'')
326 326 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
327 327 if_missing='str')
328 328 new_field_label = v.UnicodeString(not_empty=False)
329 329 new_field_desc = v.UnicodeString(not_empty=False)
330 330
331 331 return _RepoFieldForm
332 332
333 333
334 334 def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
335 335 repo_groups=[], landing_revs=[]):
336 336 class _RepoForkForm(formencode.Schema):
337 337 allow_extra_fields = True
338 338 filter_extra_fields = False
339 339 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
340 340 v.SlugifyName())
341 341 repo_group = All(v.CanWriteGroup(),
342 342 v.OneOf(repo_groups, hideList=True))
343 343 repo_type = All(v.ValidForkType(old_data), v.OneOf(supported_backends))
344 344 description = v.UnicodeString(strip=True, min=1, not_empty=True)
345 345 private = v.StringBoolean(if_missing=False)
346 346 copy_permissions = v.StringBoolean(if_missing=False)
347 347 fork_parent_id = v.UnicodeString()
348 348 chained_validators = [v.ValidForkName(edit, old_data)]
349 349 landing_rev = v.OneOf(landing_revs, hideList=True)
350 350
351 351 return _RepoForkForm
352 352
353 353
354 354 def ApplicationSettingsForm():
355 355 class _ApplicationSettingsForm(formencode.Schema):
356 356 allow_extra_fields = True
357 357 filter_extra_fields = False
358 358 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
359 359 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
360 360 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
361 361 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
362 362 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
363 363 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
364 364
365 365 return _ApplicationSettingsForm
366 366
367 367
368 368 def ApplicationVisualisationForm():
369 369 class _ApplicationVisualisationForm(formencode.Schema):
370 370 allow_extra_fields = True
371 371 filter_extra_fields = False
372 372 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
373 373 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
374 374 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
375 375
376 376 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
377 377 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
378 378 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
379 379 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
380 380 rhodecode_show_version = v.StringBoolean(if_missing=False)
381 381 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
382 382 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
383 383 rhodecode_gravatar_url = v.UnicodeString(min=3)
384 384 rhodecode_clone_uri_tmpl = v.UnicodeString(min=3)
385 385 rhodecode_support_url = v.UnicodeString()
386 386 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
387 387 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
388 388
389 389 return _ApplicationVisualisationForm
390 390
391 391
392 392 class _BaseVcsSettingsForm(formencode.Schema):
393 393 allow_extra_fields = True
394 394 filter_extra_fields = False
395 395 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
396 396 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
397 397 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
398 398
399 399 extensions_largefiles = v.StringBoolean(if_missing=False)
400 400 phases_publish = v.StringBoolean(if_missing=False)
401 401
402 402 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
403 403 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
404 404 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
405 405
406 406
407 407 def ApplicationUiSettingsForm():
408 408 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
409 409 web_push_ssl = v.StringBoolean(if_missing=False)
410 410 paths_root_path = All(
411 411 v.ValidPath(),
412 412 v.UnicodeString(strip=True, min=1, not_empty=True)
413 413 )
414 414 extensions_hgsubversion = v.StringBoolean(if_missing=False)
415 415 extensions_hggit = v.StringBoolean(if_missing=False)
416 416 new_svn_branch = v.ValidSvnPattern(section='vcs_svn_branch')
417 417 new_svn_tag = v.ValidSvnPattern(section='vcs_svn_tag')
418 418
419 419 return _ApplicationUiSettingsForm
420 420
421 421
422 422 def RepoVcsSettingsForm(repo_name):
423 423 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
424 424 inherit_global_settings = v.StringBoolean(if_missing=False)
425 425 new_svn_branch = v.ValidSvnPattern(
426 426 section='vcs_svn_branch', repo_name=repo_name)
427 427 new_svn_tag = v.ValidSvnPattern(
428 428 section='vcs_svn_tag', repo_name=repo_name)
429 429
430 430 return _RepoVcsSettingsForm
431 431
432 432
433 433 def LabsSettingsForm():
434 434 class _LabSettingsForm(formencode.Schema):
435 435 allow_extra_fields = True
436 436 filter_extra_fields = False
437 437
438 438 rhodecode_proxy_subversion_http_requests = v.StringBoolean(
439 439 if_missing=False)
440 440 rhodecode_subversion_http_server_url = v.UnicodeString(
441 441 strip=True, if_missing=None)
442 442
443 443 return _LabSettingsForm
444 444
445 445
446 446 def ApplicationPermissionsForm(register_choices, extern_activate_choices):
447 447 class _DefaultPermissionsForm(formencode.Schema):
448 448 allow_extra_fields = True
449 449 filter_extra_fields = True
450 450
451 451 anonymous = v.StringBoolean(if_missing=False)
452 452 default_register = v.OneOf(register_choices)
453 453 default_register_message = v.UnicodeString()
454 454 default_extern_activate = v.OneOf(extern_activate_choices)
455 455
456 456 return _DefaultPermissionsForm
457 457
458 458
459 459 def ObjectPermissionsForm(repo_perms_choices, group_perms_choices,
460 460 user_group_perms_choices):
461 461 class _ObjectPermissionsForm(formencode.Schema):
462 462 allow_extra_fields = True
463 463 filter_extra_fields = True
464 464 overwrite_default_repo = v.StringBoolean(if_missing=False)
465 465 overwrite_default_group = v.StringBoolean(if_missing=False)
466 466 overwrite_default_user_group = v.StringBoolean(if_missing=False)
467 467 default_repo_perm = v.OneOf(repo_perms_choices)
468 468 default_group_perm = v.OneOf(group_perms_choices)
469 469 default_user_group_perm = v.OneOf(user_group_perms_choices)
470 470
471 471 return _ObjectPermissionsForm
472 472
473 473
474 474 def UserPermissionsForm(create_choices, create_on_write_choices,
475 475 repo_group_create_choices, user_group_create_choices,
476 476 fork_choices, inherit_default_permissions_choices):
477 477 class _DefaultPermissionsForm(formencode.Schema):
478 478 allow_extra_fields = True
479 479 filter_extra_fields = True
480 480
481 481 anonymous = v.StringBoolean(if_missing=False)
482 482
483 483 default_repo_create = v.OneOf(create_choices)
484 484 default_repo_create_on_write = v.OneOf(create_on_write_choices)
485 485 default_user_group_create = v.OneOf(user_group_create_choices)
486 486 default_repo_group_create = v.OneOf(repo_group_create_choices)
487 487 default_fork_create = v.OneOf(fork_choices)
488 488 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
489 489
490 490 return _DefaultPermissionsForm
491 491
492 492
493 493 def UserIndividualPermissionsForm():
494 494 class _DefaultPermissionsForm(formencode.Schema):
495 495 allow_extra_fields = True
496 496 filter_extra_fields = True
497 497
498 498 inherit_default_permissions = v.StringBoolean(if_missing=False)
499 499
500 500 return _DefaultPermissionsForm
501 501
502 502
503 503 def DefaultsForm(edit=False, old_data={}, supported_backends=BACKENDS.keys()):
504 504 class _DefaultsForm(formencode.Schema):
505 505 allow_extra_fields = True
506 506 filter_extra_fields = True
507 507 default_repo_type = v.OneOf(supported_backends)
508 508 default_repo_private = v.StringBoolean(if_missing=False)
509 509 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
510 510 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
511 511 default_repo_enable_locking = v.StringBoolean(if_missing=False)
512 512
513 513 return _DefaultsForm
514 514
515 515
516 516 def AuthSettingsForm():
517 517 class _AuthSettingsForm(formencode.Schema):
518 518 allow_extra_fields = True
519 519 filter_extra_fields = True
520 520 auth_plugins = All(v.ValidAuthPlugins(),
521 521 v.UniqueListFromString()(not_empty=True))
522 522
523 523 return _AuthSettingsForm
524 524
525 525
526 526 def UserExtraEmailForm():
527 527 class _UserExtraEmailForm(formencode.Schema):
528 528 email = All(v.UniqSystemEmail(), v.Email(not_empty=True))
529 529 return _UserExtraEmailForm
530 530
531 531
532 532 def UserExtraIpForm():
533 533 class _UserExtraIpForm(formencode.Schema):
534 534 ip = v.ValidIp()(not_empty=True)
535 535 return _UserExtraIpForm
536 536
537 537
538 538 def PullRequestForm(repo_id):
539 539 class _PullRequestForm(formencode.Schema):
540 540 allow_extra_fields = True
541 541 filter_extra_fields = True
542 542
543 543 user = v.UnicodeString(strip=True, required=True)
544 544 source_repo = v.UnicodeString(strip=True, required=True)
545 545 source_ref = v.UnicodeString(strip=True, required=True)
546 546 target_repo = v.UnicodeString(strip=True, required=True)
547 547 target_ref = v.UnicodeString(strip=True, required=True)
548 548 revisions = All(#v.NotReviewedRevisions(repo_id)(),
549 549 v.UniqueList()(not_empty=True))
550 550 review_members = v.UniqueList(convert=int)(not_empty=True)
551 551
552 552 pullrequest_title = v.UnicodeString(strip=True, required=True)
553 553 pullrequest_desc = v.UnicodeString(strip=True, required=False)
554 554
555 555 return _PullRequestForm
556 556
557 557
558 def GistForm(lifetime_options, acl_level_options):
559 class _GistForm(formencode.Schema):
560
561 gistid = All(v.UniqGistId(), v.UnicodeString(strip=True, min=3, not_empty=False, if_missing=None))
562 filename = All(v.BasePath()(),
563 v.UnicodeString(strip=True, required=False))
564 description = v.UnicodeString(required=False, if_missing=u'')
565 lifetime = v.OneOf(lifetime_options)
566 mimetype = v.UnicodeString(required=False, if_missing=None)
567 content = v.UnicodeString(required=True, not_empty=True)
568 public = v.UnicodeString(required=False, if_missing=u'')
569 private = v.UnicodeString(required=False, if_missing=u'')
570 acl_level = v.OneOf(acl_level_options)
571
572 return _GistForm
573
574
575 558 def IssueTrackerPatternsForm():
576 559 class _IssueTrackerPatternsForm(formencode.Schema):
577 560 allow_extra_fields = True
578 561 filter_extra_fields = False
579 562 chained_validators = [v.ValidPattern()]
580 563 return _IssueTrackerPatternsForm
@@ -1,252 +1,236 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2013-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 gist model for RhodeCode
23 23 """
24 24
25 25 import os
26 26 import time
27 27 import logging
28 28 import traceback
29 29 import shutil
30 30
31 31 from rhodecode.lib.utils2 import (
32 32 safe_unicode, unique_id, safe_int, time_to_datetime, AttributeDict)
33 33 from rhodecode.lib.ext_json import json
34 34 from rhodecode.model import BaseModel
35 35 from rhodecode.model.db import Gist
36 36 from rhodecode.model.repo import RepoModel
37 37 from rhodecode.model.scm import ScmModel
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41 GIST_STORE_LOC = '.rc_gist_store'
42 42 GIST_METADATA_FILE = '.rc_gist_metadata'
43 43
44 44
45 45 class GistModel(BaseModel):
46 46 cls = Gist
47 47
48 48 def _get_gist(self, gist):
49 49 """
50 50 Helper method to get gist by ID, or gist_access_id as a fallback
51 51
52 52 :param gist: GistID, gist_access_id, or Gist instance
53 53 """
54 54 return self._get_instance(Gist, gist, callback=Gist.get_by_access_id)
55 55
56 56 def __delete_gist(self, gist):
57 57 """
58 58 removes gist from filesystem
59 59
60 60 :param gist: gist object
61 61 """
62 62 root_path = RepoModel().repos_path
63 63 rm_path = os.path.join(root_path, GIST_STORE_LOC, gist.gist_access_id)
64 64 log.info("Removing %s", rm_path)
65 65 shutil.rmtree(rm_path)
66 66
67 67 def _store_metadata(self, repo, gist_id, gist_access_id, user_id, username,
68 68 gist_type, gist_expires, gist_acl_level):
69 69 """
70 70 store metadata inside the gist repo, this can be later used for imports
71 71 or gist identification. Currently we use this inside RhodeCode tools
72 72 to do cleanup of gists that are in storage but not in database.
73 73 """
74 74 metadata = {
75 75 'metadata_version': '2',
76 76 'gist_db_id': gist_id,
77 77 'gist_access_id': gist_access_id,
78 78 'gist_owner_id': user_id,
79 79 'gist_owner_username': username,
80 80 'gist_type': gist_type,
81 81 'gist_expires': gist_expires,
82 82 'gist_updated': time.time(),
83 83 'gist_acl_level': gist_acl_level,
84 84 }
85 85 metadata_file = os.path.join(repo.path, '.hg', GIST_METADATA_FILE)
86 86 with open(metadata_file, 'wb') as f:
87 87 f.write(json.dumps(metadata))
88 88
89 89 def get_gist(self, gist):
90 90 return self._get_gist(gist)
91 91
92 92 def get_gist_files(self, gist_access_id, revision=None):
93 93 """
94 94 Get files for given gist
95 95
96 96 :param gist_access_id:
97 97 """
98 98 repo = Gist.get_by_access_id(gist_access_id)
99 99 commit = repo.scm_instance().get_commit(commit_id=revision)
100 100 return commit, [n for n in commit.get_node('/')]
101 101
102 102 def create(self, description, owner, gist_mapping,
103 103 gist_type=Gist.GIST_PUBLIC, lifetime=-1, gist_id=None,
104 104 gist_acl_level=Gist.ACL_LEVEL_PRIVATE):
105 105 """
106 106 Create a gist
107 107
108 108 :param description: description of the gist
109 109 :param owner: user who created this gist
110 :param gist_mapping: mapping {filename:{'content':content},...}
110 :param gist_mapping: mapping [{'filename': 'file1.txt', 'content': content}, ...}]
111 111 :param gist_type: type of gist private/public
112 112 :param lifetime: in minutes, -1 == forever
113 113 :param gist_acl_level: acl level for this gist
114 114 """
115 115 owner = self._get_user(owner)
116 116 gist_id = safe_unicode(gist_id or unique_id(20))
117 117 lifetime = safe_int(lifetime, -1)
118 118 gist_expires = time.time() + (lifetime * 60) if lifetime != -1 else -1
119 119 expiration = (time_to_datetime(gist_expires)
120 120 if gist_expires != -1 else 'forever')
121 121 log.debug('set GIST expiration date to: %s', expiration)
122 122 # create the Database version
123 123 gist = Gist()
124 124 gist.gist_description = description
125 125 gist.gist_access_id = gist_id
126 126 gist.gist_owner = owner.user_id
127 127 gist.gist_expires = gist_expires
128 128 gist.gist_type = safe_unicode(gist_type)
129 129 gist.acl_level = gist_acl_level
130 130 self.sa.add(gist)
131 131 self.sa.flush()
132 132 if gist_type == Gist.GIST_PUBLIC:
133 133 # use DB ID for easy to use GIST ID
134 134 gist_id = safe_unicode(gist.gist_id)
135 135 gist.gist_access_id = gist_id
136 136 self.sa.add(gist)
137 137
138 138 gist_repo_path = os.path.join(GIST_STORE_LOC, gist_id)
139 139 log.debug('Creating new %s GIST repo in %s', gist_type, gist_repo_path)
140 140 repo = RepoModel()._create_filesystem_repo(
141 141 repo_name=gist_id, repo_type='hg', repo_group=GIST_STORE_LOC,
142 142 use_global_config=True)
143 143
144 processed_mapping = {}
145 for filename in gist_mapping:
146 if filename != os.path.basename(filename):
147 raise Exception('Filename cannot be inside a directory')
148
149 content = gist_mapping[filename]['content']
150 # TODO: expand support for setting explicit lexers
151 # if lexer is None:
152 # try:
153 # guess_lexer = pygments.lexers.guess_lexer_for_filename
154 # lexer = guess_lexer(filename,content)
155 # except pygments.util.ClassNotFound:
156 # lexer = 'text'
157 processed_mapping[filename] = {'content': content}
158
159 144 # now create single multifile commit
160 145 message = 'added file'
161 message += 's: ' if len(processed_mapping) > 1 else ': '
162 message += ', '.join([x for x in processed_mapping])
146 message += 's: ' if len(gist_mapping) > 1 else ': '
147 message += ', '.join([x for x in gist_mapping])
163 148
164 149 # fake RhodeCode Repository object
165 150 fake_repo = AttributeDict({
166 151 'repo_name': gist_repo_path,
167 152 'scm_instance': lambda *args, **kwargs: repo,
168 153 })
169 154
170 155 ScmModel().create_nodes(
171 156 user=owner.user_id, repo=fake_repo,
172 157 message=message,
173 nodes=processed_mapping,
158 nodes=gist_mapping,
174 159 trigger_push_hook=False
175 160 )
176 161
177 162 self._store_metadata(repo, gist.gist_id, gist.gist_access_id,
178 163 owner.user_id, owner.username, gist.gist_type,
179 164 gist.gist_expires, gist_acl_level)
180 165 return gist
181 166
182 167 def delete(self, gist, fs_remove=True):
183 168 gist = self._get_gist(gist)
184 169 try:
185 170 self.sa.delete(gist)
186 171 if fs_remove:
187 172 self.__delete_gist(gist)
188 173 else:
189 174 log.debug('skipping removal from filesystem')
190 175 except Exception:
191 176 log.error(traceback.format_exc())
192 177 raise
193 178
194 179 def update(self, gist, description, owner, gist_mapping, gist_type,
195 180 lifetime, gist_acl_level):
196 181 gist = self._get_gist(gist)
197 182 gist_repo = gist.scm_instance()
198 183
199 lifetime = safe_int(lifetime, -1)
200 184 if lifetime == 0: # preserve old value
201 185 gist_expires = gist.gist_expires
202 186 else:
203 187 gist_expires = (
204 188 time.time() + (lifetime * 60) if lifetime != -1 else -1)
205 189
206 190 # calculate operation type based on given data
207 191 gist_mapping_op = {}
208 192 for k, v in gist_mapping.items():
209 193 # add, mod, del
210 if not v['org_filename'] and v['filename']:
194 if not v['filename_org'] and v['filename']:
211 195 op = 'add'
212 elif v['org_filename'] and not v['filename']:
196 elif v['filename_org'] and not v['filename']:
213 197 op = 'del'
214 198 else:
215 199 op = 'mod'
216 200
217 201 v['op'] = op
218 202 gist_mapping_op[k] = v
219 203
220 204 gist.gist_description = description
221 205 gist.gist_expires = gist_expires
222 206 gist.owner = owner
223 207 gist.gist_type = gist_type
224 208 gist.acl_level = gist_acl_level
225 209 self.sa.add(gist)
226 210 self.sa.flush()
227 211
228 212 message = 'updated file'
229 213 message += 's: ' if len(gist_mapping) > 1 else ': '
230 214 message += ', '.join([x for x in gist_mapping])
231 215
232 216 # fake RhodeCode Repository object
233 217 fake_repo = AttributeDict({
234 218 'repo_name': gist_repo.path,
235 219 'scm_instance': lambda *args, **kwargs: gist_repo,
236 220 })
237 221
238 222 self._store_metadata(gist_repo, gist.gist_id, gist.gist_access_id,
239 223 owner.user_id, owner.username, gist.gist_type,
240 224 gist.gist_expires, gist_acl_level)
241 225
242 226 # this can throw NodeNotChangedError, if changes we're trying to commit
243 227 # are not actually changes...
244 228 ScmModel().update_nodes(
245 229 user=owner.user_id,
246 230 repo=fake_repo,
247 231 message=message,
248 232 nodes=gist_mapping_op,
249 233 trigger_push_hook=False
250 234 )
251 235
252 236 return gist
@@ -1,1125 +1,1089 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Set of generic validators
23 23 """
24 24
25 25 import logging
26 26 import os
27 27 import re
28 28 from collections import defaultdict
29 29
30 30 import formencode
31 31 import ipaddress
32 32 from formencode.validators import (
33 33 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set,
34 34 NotEmpty, IPAddress, CIDR, String, FancyValidator
35 35 )
36 36 from pylons.i18n.translation import _
37 37 from sqlalchemy.sql.expression import true
38 38 from sqlalchemy.util import OrderedSet
39 39 from webhelpers.pylonslib.secure_form import authentication_token
40 40
41 41 from rhodecode.authentication import (
42 42 legacy_plugin_prefix, _import_legacy_plugin)
43 43 from rhodecode.authentication.base import loadplugin
44 44 from rhodecode.config.routing import ADMIN_PREFIX
45 45 from rhodecode.lib.auth import HasRepoGroupPermissionAny, HasPermissionAny
46 46 from rhodecode.lib.utils import repo_name_slug, make_db_config
47 47 from rhodecode.lib.utils2 import safe_int, str2bool, aslist, md5
48 48 from rhodecode.lib.vcs.backends.git.repository import GitRepository
49 49 from rhodecode.lib.vcs.backends.hg.repository import MercurialRepository
50 50 from rhodecode.lib.vcs.backends.svn.repository import SubversionRepository
51 51 from rhodecode.model.db import (
52 52 RepoGroup, Repository, UserGroup, User, ChangesetStatus, Gist)
53 53 from rhodecode.model.settings import VcsSettingsModel
54 54
55 55 # silence warnings and pylint
56 56 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \
57 57 NotEmpty, IPAddress, CIDR, String, FancyValidator
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61
62 62 class _Missing(object):
63 63 pass
64 64
65 65 Missing = _Missing()
66 66
67 67
68 68 class StateObj(object):
69 69 """
70 70 this is needed to translate the messages using _() in validators
71 71 """
72 72 _ = staticmethod(_)
73 73
74 74
75 75 def M(self, key, state=None, **kwargs):
76 76 """
77 77 returns string from self.message based on given key,
78 78 passed kw params are used to substitute %(named)s params inside
79 79 translated strings
80 80
81 81 :param msg:
82 82 :param state:
83 83 """
84 84 if state is None:
85 85 state = StateObj()
86 86 else:
87 87 state._ = staticmethod(_)
88 88 # inject validator into state object
89 89 return self.message(key, state, **kwargs)
90 90
91 91
92 92 def UniqueList(convert=None):
93 93 class _UniqueList(formencode.FancyValidator):
94 94 """
95 95 Unique List !
96 96 """
97 97 messages = {
98 98 'empty': _(u'Value cannot be an empty list'),
99 99 'missing_value': _(u'Value cannot be an empty list'),
100 100 }
101 101
102 102 def _to_python(self, value, state):
103 103 ret_val = []
104 104
105 105 def make_unique(value):
106 106 seen = []
107 107 return [c for c in value if not (c in seen or seen.append(c))]
108 108
109 109 if isinstance(value, list):
110 110 ret_val = make_unique(value)
111 111 elif isinstance(value, set):
112 112 ret_val = make_unique(list(value))
113 113 elif isinstance(value, tuple):
114 114 ret_val = make_unique(list(value))
115 115 elif value is None:
116 116 ret_val = []
117 117 else:
118 118 ret_val = [value]
119 119
120 120 if convert:
121 121 ret_val = map(convert, ret_val)
122 122 return ret_val
123 123
124 124 def empty_value(self, value):
125 125 return []
126 126
127 127 return _UniqueList
128 128
129 129
130 130 def UniqueListFromString():
131 131 class _UniqueListFromString(UniqueList()):
132 132 def _to_python(self, value, state):
133 133 if isinstance(value, basestring):
134 134 value = aslist(value, ',')
135 135 return super(_UniqueListFromString, self)._to_python(value, state)
136 136 return _UniqueListFromString
137 137
138 138
139 139 def ValidSvnPattern(section, repo_name=None):
140 140 class _validator(formencode.validators.FancyValidator):
141 141 messages = {
142 142 'pattern_exists': _(u'Pattern already exists'),
143 143 }
144 144
145 145 def validate_python(self, value, state):
146 146 if not value:
147 147 return
148 148 model = VcsSettingsModel(repo=repo_name)
149 149 ui_settings = model.get_svn_patterns(section=section)
150 150 for entry in ui_settings:
151 151 if value == entry.value:
152 152 msg = M(self, 'pattern_exists', state)
153 153 raise formencode.Invalid(msg, value, state)
154 154 return _validator
155 155
156 156
157 157 def ValidUsername(edit=False, old_data={}):
158 158 class _validator(formencode.validators.FancyValidator):
159 159 messages = {
160 160 'username_exists': _(u'Username "%(username)s" already exists'),
161 161 'system_invalid_username':
162 162 _(u'Username "%(username)s" is forbidden'),
163 163 'invalid_username':
164 164 _(u'Username may only contain alphanumeric characters '
165 165 u'underscores, periods or dashes and must begin with '
166 166 u'alphanumeric character or underscore')
167 167 }
168 168
169 169 def validate_python(self, value, state):
170 170 if value in ['default', 'new_user']:
171 171 msg = M(self, 'system_invalid_username', state, username=value)
172 172 raise formencode.Invalid(msg, value, state)
173 173 # check if user is unique
174 174 old_un = None
175 175 if edit:
176 176 old_un = User.get(old_data.get('user_id')).username
177 177
178 178 if old_un != value or not edit:
179 179 if User.get_by_username(value, case_insensitive=True):
180 180 msg = M(self, 'username_exists', state, username=value)
181 181 raise formencode.Invalid(msg, value, state)
182 182
183 183 if (re.match(r'^[\w]{1}[\w\-\.]{0,254}$', value)
184 184 is None):
185 185 msg = M(self, 'invalid_username', state)
186 186 raise formencode.Invalid(msg, value, state)
187 187 return _validator
188 188
189 189
190 190 def ValidRegex(msg=None):
191 191 class _validator(formencode.validators.Regex):
192 192 messages = {'invalid': msg or _(u'The input is not valid')}
193 193 return _validator
194 194
195 195
196 196 def ValidRepoUser(allow_disabled=False):
197 197 class _validator(formencode.validators.FancyValidator):
198 198 messages = {
199 199 'invalid_username': _(u'Username %(username)s is not valid'),
200 200 'disabled_username': _(u'Username %(username)s is disabled')
201 201 }
202 202
203 203 def validate_python(self, value, state):
204 204 try:
205 205 user = User.query().filter(User.username == value).one()
206 206 except Exception:
207 207 msg = M(self, 'invalid_username', state, username=value)
208 208 raise formencode.Invalid(
209 209 msg, value, state, error_dict={'username': msg}
210 210 )
211 211 if user and (not allow_disabled and not user.active):
212 212 msg = M(self, 'disabled_username', state, username=value)
213 213 raise formencode.Invalid(
214 214 msg, value, state, error_dict={'username': msg}
215 215 )
216 216
217 217 return _validator
218 218
219 219
220 220 def ValidUserGroup(edit=False, old_data={}):
221 221 class _validator(formencode.validators.FancyValidator):
222 222 messages = {
223 223 'invalid_group': _(u'Invalid user group name'),
224 224 'group_exist': _(u'User group "%(usergroup)s" already exists'),
225 225 'invalid_usergroup_name':
226 226 _(u'user group name may only contain alphanumeric '
227 227 u'characters underscores, periods or dashes and must begin '
228 228 u'with alphanumeric character')
229 229 }
230 230
231 231 def validate_python(self, value, state):
232 232 if value in ['default']:
233 233 msg = M(self, 'invalid_group', state)
234 234 raise formencode.Invalid(
235 235 msg, value, state, error_dict={'users_group_name': msg}
236 236 )
237 237 # check if group is unique
238 238 old_ugname = None
239 239 if edit:
240 240 old_id = old_data.get('users_group_id')
241 241 old_ugname = UserGroup.get(old_id).users_group_name
242 242
243 243 if old_ugname != value or not edit:
244 244 is_existing_group = UserGroup.get_by_group_name(
245 245 value, case_insensitive=True)
246 246 if is_existing_group:
247 247 msg = M(self, 'group_exist', state, usergroup=value)
248 248 raise formencode.Invalid(
249 249 msg, value, state, error_dict={'users_group_name': msg}
250 250 )
251 251
252 252 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
253 253 msg = M(self, 'invalid_usergroup_name', state)
254 254 raise formencode.Invalid(
255 255 msg, value, state, error_dict={'users_group_name': msg}
256 256 )
257 257
258 258 return _validator
259 259
260 260
261 261 def ValidRepoGroup(edit=False, old_data={}, can_create_in_root=False):
262 262 class _validator(formencode.validators.FancyValidator):
263 263 messages = {
264 264 'group_parent_id': _(u'Cannot assign this group as parent'),
265 265 'group_exists': _(u'Group "%(group_name)s" already exists'),
266 266 'repo_exists': _(u'Repository with name "%(group_name)s" '
267 267 u'already exists'),
268 268 'permission_denied': _(u"no permission to store repository group"
269 269 u"in this location"),
270 270 'permission_denied_root': _(
271 271 u"no permission to store repository group "
272 272 u"in root location")
273 273 }
274 274
275 275 def _to_python(self, value, state):
276 276 group_name = repo_name_slug(value.get('group_name', ''))
277 277 group_parent_id = safe_int(value.get('group_parent_id'))
278 278 gr = RepoGroup.get(group_parent_id)
279 279 if gr:
280 280 parent_group_path = gr.full_path
281 281 # value needs to be aware of group name in order to check
282 282 # db key This is an actual just the name to store in the
283 283 # database
284 284 group_name_full = (
285 285 parent_group_path + RepoGroup.url_sep() + group_name)
286 286 else:
287 287 group_name_full = group_name
288 288
289 289 value['group_name'] = group_name
290 290 value['group_name_full'] = group_name_full
291 291 value['group_parent_id'] = group_parent_id
292 292 return value
293 293
294 294 def validate_python(self, value, state):
295 295
296 296 old_group_name = None
297 297 group_name = value.get('group_name')
298 298 group_name_full = value.get('group_name_full')
299 299 group_parent_id = safe_int(value.get('group_parent_id'))
300 300 if group_parent_id == -1:
301 301 group_parent_id = None
302 302
303 303 group_obj = RepoGroup.get(old_data.get('group_id'))
304 304 parent_group_changed = False
305 305 if edit:
306 306 old_group_name = group_obj.group_name
307 307 old_group_parent_id = group_obj.group_parent_id
308 308
309 309 if group_parent_id != old_group_parent_id:
310 310 parent_group_changed = True
311 311
312 312 # TODO: mikhail: the following if statement is not reached
313 313 # since group_parent_id's OneOf validation fails before.
314 314 # Can be removed.
315 315
316 316 # check against setting a parent of self
317 317 parent_of_self = (
318 318 old_data['group_id'] == group_parent_id
319 319 if group_parent_id else False
320 320 )
321 321 if parent_of_self:
322 322 msg = M(self, 'group_parent_id', state)
323 323 raise formencode.Invalid(
324 324 msg, value, state, error_dict={'group_parent_id': msg}
325 325 )
326 326
327 327 # group we're moving current group inside
328 328 child_group = None
329 329 if group_parent_id:
330 330 child_group = RepoGroup.query().filter(
331 331 RepoGroup.group_id == group_parent_id).scalar()
332 332
333 333 # do a special check that we cannot move a group to one of
334 334 # it's children
335 335 if edit and child_group:
336 336 parents = [x.group_id for x in child_group.parents]
337 337 move_to_children = old_data['group_id'] in parents
338 338 if move_to_children:
339 339 msg = M(self, 'group_parent_id', state)
340 340 raise formencode.Invalid(
341 341 msg, value, state, error_dict={'group_parent_id': msg})
342 342
343 343 # Check if we have permission to store in the parent.
344 344 # Only check if the parent group changed.
345 345 if parent_group_changed:
346 346 if child_group is None:
347 347 if not can_create_in_root:
348 348 msg = M(self, 'permission_denied_root', state)
349 349 raise formencode.Invalid(
350 350 msg, value, state,
351 351 error_dict={'group_parent_id': msg})
352 352 else:
353 353 valid = HasRepoGroupPermissionAny('group.admin')
354 354 forbidden = not valid(
355 355 child_group.group_name, 'can create group validator')
356 356 if forbidden:
357 357 msg = M(self, 'permission_denied', state)
358 358 raise formencode.Invalid(
359 359 msg, value, state,
360 360 error_dict={'group_parent_id': msg})
361 361
362 362 # if we change the name or it's new group, check for existing names
363 363 # or repositories with the same name
364 364 if old_group_name != group_name_full or not edit:
365 365 # check group
366 366 gr = RepoGroup.get_by_group_name(group_name_full)
367 367 if gr:
368 368 msg = M(self, 'group_exists', state, group_name=group_name)
369 369 raise formencode.Invalid(
370 370 msg, value, state, error_dict={'group_name': msg})
371 371
372 372 # check for same repo
373 373 repo = Repository.get_by_repo_name(group_name_full)
374 374 if repo:
375 375 msg = M(self, 'repo_exists', state, group_name=group_name)
376 376 raise formencode.Invalid(
377 377 msg, value, state, error_dict={'group_name': msg})
378 378
379 379 return _validator
380 380
381 381
382 382 def ValidPassword():
383 383 class _validator(formencode.validators.FancyValidator):
384 384 messages = {
385 385 'invalid_password':
386 386 _(u'Invalid characters (non-ascii) in password')
387 387 }
388 388
389 389 def validate_python(self, value, state):
390 390 try:
391 391 (value or '').decode('ascii')
392 392 except UnicodeError:
393 393 msg = M(self, 'invalid_password', state)
394 394 raise formencode.Invalid(msg, value, state,)
395 395 return _validator
396 396
397 397
398 398 def ValidOldPassword(username):
399 399 class _validator(formencode.validators.FancyValidator):
400 400 messages = {
401 401 'invalid_password': _(u'Invalid old password')
402 402 }
403 403
404 404 def validate_python(self, value, state):
405 405 from rhodecode.authentication.base import authenticate, HTTP_TYPE
406 406 if not authenticate(username, value, '', HTTP_TYPE):
407 407 msg = M(self, 'invalid_password', state)
408 408 raise formencode.Invalid(
409 409 msg, value, state, error_dict={'current_password': msg}
410 410 )
411 411 return _validator
412 412
413 413
414 414 def ValidPasswordsMatch(
415 415 passwd='new_password', passwd_confirmation='password_confirmation'):
416 416 class _validator(formencode.validators.FancyValidator):
417 417 messages = {
418 418 'password_mismatch': _(u'Passwords do not match'),
419 419 }
420 420
421 421 def validate_python(self, value, state):
422 422
423 423 pass_val = value.get('password') or value.get(passwd)
424 424 if pass_val != value[passwd_confirmation]:
425 425 msg = M(self, 'password_mismatch', state)
426 426 raise formencode.Invalid(
427 427 msg, value, state,
428 428 error_dict={passwd: msg, passwd_confirmation: msg}
429 429 )
430 430 return _validator
431 431
432 432
433 433 def ValidAuth():
434 434 class _validator(formencode.validators.FancyValidator):
435 435 messages = {
436 436 'invalid_password': _(u'invalid password'),
437 437 'invalid_username': _(u'invalid user name'),
438 438 'disabled_account': _(u'Your account is disabled')
439 439 }
440 440
441 441 def validate_python(self, value, state):
442 442 from rhodecode.authentication.base import authenticate, HTTP_TYPE
443 443
444 444 password = value['password']
445 445 username = value['username']
446 446
447 447 if not authenticate(username, password, '', HTTP_TYPE,
448 448 skip_missing=True):
449 449 user = User.get_by_username(username)
450 450 if user and not user.active:
451 451 log.warning('user %s is disabled', username)
452 452 msg = M(self, 'disabled_account', state)
453 453 raise formencode.Invalid(
454 454 msg, value, state, error_dict={'username': msg}
455 455 )
456 456 else:
457 457 log.warning('user `%s` failed to authenticate', username)
458 458 msg = M(self, 'invalid_username', state)
459 459 msg2 = M(self, 'invalid_password', state)
460 460 raise formencode.Invalid(
461 461 msg, value, state,
462 462 error_dict={'username': msg, 'password': msg2}
463 463 )
464 464 return _validator
465 465
466 466
467 467 def ValidAuthToken():
468 468 class _validator(formencode.validators.FancyValidator):
469 469 messages = {
470 470 'invalid_token': _(u'Token mismatch')
471 471 }
472 472
473 473 def validate_python(self, value, state):
474 474 if value != authentication_token():
475 475 msg = M(self, 'invalid_token', state)
476 476 raise formencode.Invalid(msg, value, state)
477 477 return _validator
478 478
479 479
480 480 def ValidRepoName(edit=False, old_data={}):
481 481 class _validator(formencode.validators.FancyValidator):
482 482 messages = {
483 483 'invalid_repo_name':
484 484 _(u'Repository name %(repo)s is disallowed'),
485 485 # top level
486 486 'repository_exists': _(u'Repository with name %(repo)s '
487 487 u'already exists'),
488 488 'group_exists': _(u'Repository group with name "%(repo)s" '
489 489 u'already exists'),
490 490 # inside a group
491 491 'repository_in_group_exists': _(u'Repository with name %(repo)s '
492 492 u'exists in group "%(group)s"'),
493 493 'group_in_group_exists': _(
494 494 u'Repository group with name "%(repo)s" '
495 495 u'exists in group "%(group)s"'),
496 496 }
497 497
498 498 def _to_python(self, value, state):
499 499 repo_name = repo_name_slug(value.get('repo_name', ''))
500 500 repo_group = value.get('repo_group')
501 501 if repo_group:
502 502 gr = RepoGroup.get(repo_group)
503 503 group_path = gr.full_path
504 504 group_name = gr.group_name
505 505 # value needs to be aware of group name in order to check
506 506 # db key This is an actual just the name to store in the
507 507 # database
508 508 repo_name_full = group_path + RepoGroup.url_sep() + repo_name
509 509 else:
510 510 group_name = group_path = ''
511 511 repo_name_full = repo_name
512 512
513 513 value['repo_name'] = repo_name
514 514 value['repo_name_full'] = repo_name_full
515 515 value['group_path'] = group_path
516 516 value['group_name'] = group_name
517 517 return value
518 518
519 519 def validate_python(self, value, state):
520 520
521 521 repo_name = value.get('repo_name')
522 522 repo_name_full = value.get('repo_name_full')
523 523 group_path = value.get('group_path')
524 524 group_name = value.get('group_name')
525 525
526 526 if repo_name in [ADMIN_PREFIX, '']:
527 527 msg = M(self, 'invalid_repo_name', state, repo=repo_name)
528 528 raise formencode.Invalid(
529 529 msg, value, state, error_dict={'repo_name': msg})
530 530
531 531 rename = old_data.get('repo_name') != repo_name_full
532 532 create = not edit
533 533 if rename or create:
534 534
535 535 if group_path:
536 536 if Repository.get_by_repo_name(repo_name_full):
537 537 msg = M(self, 'repository_in_group_exists', state,
538 538 repo=repo_name, group=group_name)
539 539 raise formencode.Invalid(
540 540 msg, value, state, error_dict={'repo_name': msg})
541 541 if RepoGroup.get_by_group_name(repo_name_full):
542 542 msg = M(self, 'group_in_group_exists', state,
543 543 repo=repo_name, group=group_name)
544 544 raise formencode.Invalid(
545 545 msg, value, state, error_dict={'repo_name': msg})
546 546 else:
547 547 if RepoGroup.get_by_group_name(repo_name_full):
548 548 msg = M(self, 'group_exists', state, repo=repo_name)
549 549 raise formencode.Invalid(
550 550 msg, value, state, error_dict={'repo_name': msg})
551 551
552 552 if Repository.get_by_repo_name(repo_name_full):
553 553 msg = M(
554 554 self, 'repository_exists', state, repo=repo_name)
555 555 raise formencode.Invalid(
556 556 msg, value, state, error_dict={'repo_name': msg})
557 557 return value
558 558 return _validator
559 559
560 560
561 561 def ValidForkName(*args, **kwargs):
562 562 return ValidRepoName(*args, **kwargs)
563 563
564 564
565 565 def SlugifyName():
566 566 class _validator(formencode.validators.FancyValidator):
567 567
568 568 def _to_python(self, value, state):
569 569 return repo_name_slug(value)
570 570
571 571 def validate_python(self, value, state):
572 572 pass
573 573
574 574 return _validator
575 575
576 576
577 577 def ValidCloneUri():
578 578 class InvalidCloneUrl(Exception):
579 579 allowed_prefixes = ()
580 580
581 581 def url_handler(repo_type, url):
582 582 config = make_db_config(clear_session=False)
583 583 if repo_type == 'hg':
584 584 allowed_prefixes = ('http', 'svn+http', 'git+http')
585 585
586 586 if 'http' in url[:4]:
587 587 # initially check if it's at least the proper URL
588 588 # or does it pass basic auth
589 589 MercurialRepository.check_url(url, config)
590 590 elif 'svn+http' in url[:8]: # svn->hg import
591 591 SubversionRepository.check_url(url, config)
592 592 elif 'git+http' in url[:8]: # git->hg import
593 593 raise NotImplementedError()
594 594 else:
595 595 exc = InvalidCloneUrl('Clone from URI %s not allowed. '
596 596 'Allowed url must start with one of %s'
597 597 % (url, ','.join(allowed_prefixes)))
598 598 exc.allowed_prefixes = allowed_prefixes
599 599 raise exc
600 600
601 601 elif repo_type == 'git':
602 602 allowed_prefixes = ('http', 'svn+http', 'hg+http')
603 603 if 'http' in url[:4]:
604 604 # initially check if it's at least the proper URL
605 605 # or does it pass basic auth
606 606 GitRepository.check_url(url, config)
607 607 elif 'svn+http' in url[:8]: # svn->git import
608 608 raise NotImplementedError()
609 609 elif 'hg+http' in url[:8]: # hg->git import
610 610 raise NotImplementedError()
611 611 else:
612 612 exc = InvalidCloneUrl('Clone from URI %s not allowed. '
613 613 'Allowed url must start with one of %s'
614 614 % (url, ','.join(allowed_prefixes)))
615 615 exc.allowed_prefixes = allowed_prefixes
616 616 raise exc
617 617
618 618 class _validator(formencode.validators.FancyValidator):
619 619 messages = {
620 620 'clone_uri': _(u'invalid clone url for %(rtype)s repository'),
621 621 'invalid_clone_uri': _(
622 622 u'Invalid clone url, provide a valid clone '
623 623 u'url starting with one of %(allowed_prefixes)s')
624 624 }
625 625
626 626 def validate_python(self, value, state):
627 627 repo_type = value.get('repo_type')
628 628 url = value.get('clone_uri')
629 629
630 630 if url:
631 631 try:
632 632 url_handler(repo_type, url)
633 633 except InvalidCloneUrl as e:
634 634 log.warning(e)
635 635 msg = M(self, 'invalid_clone_uri', rtype=repo_type,
636 636 allowed_prefixes=','.join(e.allowed_prefixes))
637 637 raise formencode.Invalid(msg, value, state,
638 638 error_dict={'clone_uri': msg})
639 639 except Exception:
640 640 log.exception('Url validation failed')
641 641 msg = M(self, 'clone_uri', rtype=repo_type)
642 642 raise formencode.Invalid(msg, value, state,
643 643 error_dict={'clone_uri': msg})
644 644 return _validator
645 645
646 646
647 647 def ValidForkType(old_data={}):
648 648 class _validator(formencode.validators.FancyValidator):
649 649 messages = {
650 650 'invalid_fork_type': _(u'Fork have to be the same type as parent')
651 651 }
652 652
653 653 def validate_python(self, value, state):
654 654 if old_data['repo_type'] != value:
655 655 msg = M(self, 'invalid_fork_type', state)
656 656 raise formencode.Invalid(
657 657 msg, value, state, error_dict={'repo_type': msg}
658 658 )
659 659 return _validator
660 660
661 661
662 662 def CanWriteGroup(old_data=None):
663 663 class _validator(formencode.validators.FancyValidator):
664 664 messages = {
665 665 'permission_denied': _(
666 666 u"You do not have the permission "
667 667 u"to create repositories in this group."),
668 668 'permission_denied_root': _(
669 669 u"You do not have the permission to store repositories in "
670 670 u"the root location.")
671 671 }
672 672
673 673 def _to_python(self, value, state):
674 674 # root location
675 675 if value in [-1, "-1"]:
676 676 return None
677 677 return value
678 678
679 679 def validate_python(self, value, state):
680 680 gr = RepoGroup.get(value)
681 681 gr_name = gr.group_name if gr else None # None means ROOT location
682 682 # create repositories with write permission on group is set to true
683 683 create_on_write = HasPermissionAny(
684 684 'hg.create.write_on_repogroup.true')()
685 685 group_admin = HasRepoGroupPermissionAny('group.admin')(
686 686 gr_name, 'can write into group validator')
687 687 group_write = HasRepoGroupPermissionAny('group.write')(
688 688 gr_name, 'can write into group validator')
689 689 forbidden = not (group_admin or (group_write and create_on_write))
690 690 can_create_repos = HasPermissionAny(
691 691 'hg.admin', 'hg.create.repository')
692 692 gid = (old_data['repo_group'].get('group_id')
693 693 if (old_data and 'repo_group' in old_data) else None)
694 694 value_changed = gid != safe_int(value)
695 695 new = not old_data
696 696 # do check if we changed the value, there's a case that someone got
697 697 # revoked write permissions to a repository, he still created, we
698 698 # don't need to check permission if he didn't change the value of
699 699 # groups in form box
700 700 if value_changed or new:
701 701 # parent group need to be existing
702 702 if gr and forbidden:
703 703 msg = M(self, 'permission_denied', state)
704 704 raise formencode.Invalid(
705 705 msg, value, state, error_dict={'repo_type': msg}
706 706 )
707 707 # check if we can write to root location !
708 708 elif gr is None and not can_create_repos():
709 709 msg = M(self, 'permission_denied_root', state)
710 710 raise formencode.Invalid(
711 711 msg, value, state, error_dict={'repo_type': msg}
712 712 )
713 713
714 714 return _validator
715 715
716 716
717 717 def ValidPerms(type_='repo'):
718 718 if type_ == 'repo_group':
719 719 EMPTY_PERM = 'group.none'
720 720 elif type_ == 'repo':
721 721 EMPTY_PERM = 'repository.none'
722 722 elif type_ == 'user_group':
723 723 EMPTY_PERM = 'usergroup.none'
724 724
725 725 class _validator(formencode.validators.FancyValidator):
726 726 messages = {
727 727 'perm_new_member_name':
728 728 _(u'This username or user group name is not valid')
729 729 }
730 730
731 731 def _to_python(self, value, state):
732 732 perm_updates = OrderedSet()
733 733 perm_additions = OrderedSet()
734 734 perm_deletions = OrderedSet()
735 735 # build a list of permission to update/delete and new permission
736 736
737 737 # Read the perm_new_member/perm_del_member attributes and group
738 738 # them by they IDs
739 739 new_perms_group = defaultdict(dict)
740 740 del_perms_group = defaultdict(dict)
741 741 for k, v in value.copy().iteritems():
742 742 if k.startswith('perm_del_member'):
743 743 # delete from org storage so we don't process that later
744 744 del value[k]
745 745 # part is `id`, `type`
746 746 _type, part = k.split('perm_del_member_')
747 747 args = part.split('_')
748 748 if len(args) == 2:
749 749 _key, pos = args
750 750 del_perms_group[pos][_key] = v
751 751 if k.startswith('perm_new_member'):
752 752 # delete from org storage so we don't process that later
753 753 del value[k]
754 754 # part is `id`, `type`, `perm`
755 755 _type, part = k.split('perm_new_member_')
756 756 args = part.split('_')
757 757 if len(args) == 2:
758 758 _key, pos = args
759 759 new_perms_group[pos][_key] = v
760 760
761 761 # store the deletes
762 762 for k in sorted(del_perms_group.keys()):
763 763 perm_dict = del_perms_group[k]
764 764 del_member = perm_dict.get('id')
765 765 del_type = perm_dict.get('type')
766 766 if del_member and del_type:
767 767 perm_deletions.add((del_member, None, del_type))
768 768
769 769 # store additions in order of how they were added in web form
770 770 for k in sorted(new_perms_group.keys()):
771 771 perm_dict = new_perms_group[k]
772 772 new_member = perm_dict.get('id')
773 773 new_type = perm_dict.get('type')
774 774 new_perm = perm_dict.get('perm')
775 775 if new_member and new_perm and new_type:
776 776 perm_additions.add((new_member, new_perm, new_type))
777 777
778 778 # get updates of permissions
779 779 # (read the existing radio button states)
780 780 for k, update_value in value.iteritems():
781 781 if k.startswith('u_perm_') or k.startswith('g_perm_'):
782 782 member = k[7:]
783 783 update_type = {'u': 'user',
784 784 'g': 'users_group'}[k[0]]
785 785 if member == User.DEFAULT_USER:
786 786 if str2bool(value.get('repo_private')):
787 787 # set none for default when updating to
788 788 # private repo protects agains form manipulation
789 789 update_value = EMPTY_PERM
790 790 perm_updates.add((member, update_value, update_type))
791 791 # check the deletes
792 792
793 793 value['perm_additions'] = list(perm_additions)
794 794 value['perm_updates'] = list(perm_updates)
795 795 value['perm_deletions'] = list(perm_deletions)
796 796
797 797 # validate users they exist and they are active !
798 798 for member_id, _perm, member_type in perm_additions:
799 799 try:
800 800 if member_type == 'user':
801 801 self.user_db = User.query()\
802 802 .filter(User.active == true())\
803 803 .filter(User.user_id == member_id).one()
804 804 if member_type == 'users_group':
805 805 self.user_db = UserGroup.query()\
806 806 .filter(UserGroup.users_group_active == true())\
807 807 .filter(UserGroup.users_group_id == member_id)\
808 808 .one()
809 809
810 810 except Exception:
811 811 log.exception('Updated permission failed: org_exc:')
812 812 msg = M(self, 'perm_new_member_type', state)
813 813 raise formencode.Invalid(
814 814 msg, value, state, error_dict={
815 815 'perm_new_member_name': msg}
816 816 )
817 817 return value
818 818 return _validator
819 819
820 820
821 821 def ValidSettings():
822 822 class _validator(formencode.validators.FancyValidator):
823 823 def _to_python(self, value, state):
824 824 # settings form for users that are not admin
825 825 # can't edit certain parameters, it's extra backup if they mangle
826 826 # with forms
827 827
828 828 forbidden_params = [
829 829 'user', 'repo_type', 'repo_enable_locking',
830 830 'repo_enable_downloads', 'repo_enable_statistics'
831 831 ]
832 832
833 833 for param in forbidden_params:
834 834 if param in value:
835 835 del value[param]
836 836 return value
837 837
838 838 def validate_python(self, value, state):
839 839 pass
840 840 return _validator
841 841
842 842
843 843 def ValidPath():
844 844 class _validator(formencode.validators.FancyValidator):
845 845 messages = {
846 846 'invalid_path': _(u'This is not a valid path')
847 847 }
848 848
849 849 def validate_python(self, value, state):
850 850 if not os.path.isdir(value):
851 851 msg = M(self, 'invalid_path', state)
852 852 raise formencode.Invalid(
853 853 msg, value, state, error_dict={'paths_root_path': msg}
854 854 )
855 855 return _validator
856 856
857 857
858 858 def UniqSystemEmail(old_data={}):
859 859 class _validator(formencode.validators.FancyValidator):
860 860 messages = {
861 861 'email_taken': _(u'This e-mail address is already taken')
862 862 }
863 863
864 864 def _to_python(self, value, state):
865 865 return value.lower()
866 866
867 867 def validate_python(self, value, state):
868 868 if (old_data.get('email') or '').lower() != value:
869 869 user = User.get_by_email(value, case_insensitive=True)
870 870 if user:
871 871 msg = M(self, 'email_taken', state)
872 872 raise formencode.Invalid(
873 873 msg, value, state, error_dict={'email': msg}
874 874 )
875 875 return _validator
876 876
877 877
878 878 def ValidSystemEmail():
879 879 class _validator(formencode.validators.FancyValidator):
880 880 messages = {
881 881 'non_existing_email': _(u'e-mail "%(email)s" does not exist.')
882 882 }
883 883
884 884 def _to_python(self, value, state):
885 885 return value.lower()
886 886
887 887 def validate_python(self, value, state):
888 888 user = User.get_by_email(value, case_insensitive=True)
889 889 if user is None:
890 890 msg = M(self, 'non_existing_email', state, email=value)
891 891 raise formencode.Invalid(
892 892 msg, value, state, error_dict={'email': msg}
893 893 )
894 894
895 895 return _validator
896 896
897 897
898 898 def NotReviewedRevisions(repo_id):
899 899 class _validator(formencode.validators.FancyValidator):
900 900 messages = {
901 901 'rev_already_reviewed':
902 902 _(u'Revisions %(revs)s are already part of pull request '
903 903 u'or have set status'),
904 904 }
905 905
906 906 def validate_python(self, value, state):
907 907 # check revisions if they are not reviewed, or a part of another
908 908 # pull request
909 909 statuses = ChangesetStatus.query()\
910 910 .filter(ChangesetStatus.revision.in_(value))\
911 911 .filter(ChangesetStatus.repo_id == repo_id)\
912 912 .all()
913 913
914 914 errors = []
915 915 for status in statuses:
916 916 if status.pull_request_id:
917 917 errors.append(['pull_req', status.revision[:12]])
918 918 elif status.status:
919 919 errors.append(['status', status.revision[:12]])
920 920
921 921 if errors:
922 922 revs = ','.join([x[1] for x in errors])
923 923 msg = M(self, 'rev_already_reviewed', state, revs=revs)
924 924 raise formencode.Invalid(
925 925 msg, value, state, error_dict={'revisions': revs})
926 926
927 927 return _validator
928 928
929 929
930 930 def ValidIp():
931 931 class _validator(CIDR):
932 932 messages = {
933 933 'badFormat': _(u'Please enter a valid IPv4 or IpV6 address'),
934 934 'illegalBits': _(
935 935 u'The network size (bits) must be within the range '
936 936 u'of 0-32 (not %(bits)r)'),
937 937 }
938 938
939 939 # we ovveride the default to_python() call
940 940 def to_python(self, value, state):
941 941 v = super(_validator, self).to_python(value, state)
942 942 v = v.strip()
943 943 net = ipaddress.ip_network(address=v, strict=False)
944 944 return str(net)
945 945
946 946 def validate_python(self, value, state):
947 947 try:
948 948 addr = value.strip()
949 949 # this raises an ValueError if address is not IpV4 or IpV6
950 950 ipaddress.ip_network(addr, strict=False)
951 951 except ValueError:
952 952 raise formencode.Invalid(self.message('badFormat', state),
953 953 value, state)
954 954
955 955 return _validator
956 956
957 957
958 958 def FieldKey():
959 959 class _validator(formencode.validators.FancyValidator):
960 960 messages = {
961 961 'badFormat': _(
962 962 u'Key name can only consist of letters, '
963 963 u'underscore, dash or numbers'),
964 964 }
965 965
966 966 def validate_python(self, value, state):
967 967 if not re.match('[a-zA-Z0-9_-]+$', value):
968 968 raise formencode.Invalid(self.message('badFormat', state),
969 969 value, state)
970 970 return _validator
971 971
972 972
973 def BasePath():
974 class _validator(formencode.validators.FancyValidator):
975 messages = {
976 'badPath': _(u'Filename cannot be inside a directory'),
977 }
978
979 def _to_python(self, value, state):
980 return value
981
982 def validate_python(self, value, state):
983 if value != os.path.basename(value):
984 raise formencode.Invalid(self.message('badPath', state),
985 value, state)
986 return _validator
987
988
989 973 def ValidAuthPlugins():
990 974 class _validator(formencode.validators.FancyValidator):
991 975 messages = {
992 976 'import_duplicate': _(
993 977 u'Plugins %(loaded)s and %(next_to_load)s '
994 978 u'both export the same name'),
995 979 'missing_includeme': _(
996 980 u'The plugin "%(plugin_id)s" is missing an includeme '
997 981 u'function.'),
998 982 'import_error': _(
999 983 u'Can not load plugin "%(plugin_id)s"'),
1000 984 'no_plugin': _(
1001 985 u'No plugin available with ID "%(plugin_id)s"'),
1002 986 }
1003 987
1004 988 def _to_python(self, value, state):
1005 989 # filter empty values
1006 990 return filter(lambda s: s not in [None, ''], value)
1007 991
1008 992 def _validate_legacy_plugin_id(self, plugin_id, value, state):
1009 993 """
1010 994 Validates that the plugin import works. It also checks that the
1011 995 plugin has an includeme attribute.
1012 996 """
1013 997 try:
1014 998 plugin = _import_legacy_plugin(plugin_id)
1015 999 except Exception as e:
1016 1000 log.exception(
1017 1001 'Exception during import of auth legacy plugin "{}"'
1018 1002 .format(plugin_id))
1019 1003 msg = M(self, 'import_error', plugin_id=plugin_id)
1020 1004 raise formencode.Invalid(msg, value, state)
1021 1005
1022 1006 if not hasattr(plugin, 'includeme'):
1023 1007 msg = M(self, 'missing_includeme', plugin_id=plugin_id)
1024 1008 raise formencode.Invalid(msg, value, state)
1025 1009
1026 1010 return plugin
1027 1011
1028 1012 def _validate_plugin_id(self, plugin_id, value, state):
1029 1013 """
1030 1014 Plugins are already imported during app start up. Therefore this
1031 1015 validation only retrieves the plugin from the plugin registry and
1032 1016 if it returns something not None everything is OK.
1033 1017 """
1034 1018 plugin = loadplugin(plugin_id)
1035 1019
1036 1020 if plugin is None:
1037 1021 msg = M(self, 'no_plugin', plugin_id=plugin_id)
1038 1022 raise formencode.Invalid(msg, value, state)
1039 1023
1040 1024 return plugin
1041 1025
1042 1026 def validate_python(self, value, state):
1043 1027 unique_names = {}
1044 1028 for plugin_id in value:
1045 1029
1046 1030 # Validate legacy or normal plugin.
1047 1031 if plugin_id.startswith(legacy_plugin_prefix):
1048 1032 plugin = self._validate_legacy_plugin_id(
1049 1033 plugin_id, value, state)
1050 1034 else:
1051 1035 plugin = self._validate_plugin_id(plugin_id, value, state)
1052 1036
1053 1037 # Only allow unique plugin names.
1054 1038 if plugin.name in unique_names:
1055 1039 msg = M(self, 'import_duplicate', state,
1056 1040 loaded=unique_names[plugin.name],
1057 1041 next_to_load=plugin)
1058 1042 raise formencode.Invalid(msg, value, state)
1059 1043 unique_names[plugin.name] = plugin
1060 1044
1061 1045 return _validator
1062 1046
1063 1047
1064 def UniqGistId():
1065 class _validator(formencode.validators.FancyValidator):
1066 messages = {
1067 'gistid_taken': _(u'This gistid is already in use')
1068 }
1069
1070 def _to_python(self, value, state):
1071 return repo_name_slug(value.lower())
1072
1073 def validate_python(self, value, state):
1074 existing = Gist.get_by_access_id(value)
1075 if existing:
1076 msg = M(self, 'gistid_taken', state)
1077 raise formencode.Invalid(
1078 msg, value, state, error_dict={'gistid': msg}
1079 )
1080
1081 return _validator
1082
1083
1084 1048 def ValidPattern():
1085 1049
1086 1050 class _Validator(formencode.validators.FancyValidator):
1087 1051
1088 1052 def _to_python(self, value, state):
1089 1053 patterns = []
1090 1054
1091 1055 prefix = 'new_pattern'
1092 1056 for name, v in value.iteritems():
1093 1057 pattern_name = '_'.join((prefix, 'pattern'))
1094 1058 if name.startswith(pattern_name):
1095 1059 new_item_id = name[len(pattern_name)+1:]
1096 1060
1097 1061 def _field(name):
1098 1062 return '%s_%s_%s' % (prefix, name, new_item_id)
1099 1063
1100 1064 values = {
1101 1065 'issuetracker_pat': value.get(_field('pattern')),
1102 1066 'issuetracker_pat': value.get(_field('pattern')),
1103 1067 'issuetracker_url': value.get(_field('url')),
1104 1068 'issuetracker_pref': value.get(_field('prefix')),
1105 1069 'issuetracker_desc': value.get(_field('description'))
1106 1070 }
1107 1071 new_uid = md5(values['issuetracker_pat'])
1108 1072
1109 1073 has_required_fields = (
1110 1074 values['issuetracker_pat']
1111 1075 and values['issuetracker_url'])
1112 1076
1113 1077 if has_required_fields:
1114 1078 settings = [
1115 1079 ('_'.join((key, new_uid)), values[key], 'unicode')
1116 1080 for key in values]
1117 1081 patterns.append(settings)
1118 1082
1119 1083 value['patterns'] = patterns
1120 1084 delete_patterns = value.get('uid') or []
1121 1085 if not isinstance(delete_patterns, (list, tuple)):
1122 1086 delete_patterns = [delete_patterns]
1123 1087 value['delete_patterns'] = delete_patterns
1124 1088 return value
1125 1089 return _Validator
@@ -1,136 +1,140 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.html"/>
3 3
4 4 <%def name="title()">
5 5 ${_('Edit Gist')} &middot; ${c.gist.gist_access_id}
6 6 %if c.rhodecode_name:
7 7 &middot; ${h.branding(c.rhodecode_name)}
8 8 %endif
9 9 </%def>
10 10
11 11 <%def name="breadcrumbs_links()">
12 12 ${_('Edit Gist')} &middot; ${c.gist.gist_access_id}
13 13 </%def>
14 14
15 15 <%def name="menu_bar_nav()">
16 16 ${self.menu_items(active='gists')}
17 17 </%def>
18 18
19 19 <%def name="main()">
20 20 <div class="box">
21 21 <!-- box / title -->
22 22 <div class="title">
23 23 ${self.breadcrumbs()}
24 24 </div>
25 25
26 26 <div class="table">
27 27 <div id="edit_error" class="flash_msg" style="display:none;">
28 28 <div class="alert alert-warning">
29 29 ${h.literal(_('Gist was updated since you started editing. Copy your changes and click %(here)s to reload the new version.')
30 30 % {'here': h.link_to('here',h.url('edit_gist', gist_id=c.gist.gist_access_id))})}
31 31 </div>
32 32 </div>
33 33
34 34 <div id="files_data">
35 35 ${h.secure_form(h.url('edit_gist', gist_id=c.gist.gist_access_id), method='post', id='eform')}
36 36 <div>
37 37 <input type="hidden" value="${c.file_last_commit.raw_id}" name="parent_hash">
38 38 <textarea id="description" name="description"
39 39 placeholder="${_('Gist description ...')}">${c.gist.gist_description}</textarea>
40 40 <div>
41 41 <span class="gist-gravatar">
42 42 ${self.gravatar(h.email_or_none(c.rhodecode_user.full_contact), 30)}
43 43 </span>
44 44 <label for='lifetime'>${_('Gist lifetime')}</label>
45 45 ${h.dropdownmenu('lifetime', '0', c.lifetime_options)}
46 46
47 <label for='acl_level'>${_('Gist access level')}</label>
48 ${h.dropdownmenu('acl_level', c.gist.acl_level, c.acl_options)}
47 <label for='gist_acl_level'>${_('Gist access level')}</label>
48 ${h.dropdownmenu('gist_acl_level', c.gist.acl_level, c.acl_options)}
49 49 </div>
50 50 </div>
51 51
52 ## peppercorn schema
53 <input type="hidden" name="__start__" value="nodes:sequence"/>
52 54 % for cnt, file in enumerate(c.files):
55 <input type="hidden" name="__start__" value="file:mapping"/>
53 56 <div id="codeblock" class="codeblock" >
54 57 <div class="code-header">
55 58 <div class="form">
56 59 <div class="fields">
57 <input type="hidden" value="${file.path}" name="org_files">
58 <input id="filename_${h.FID('f',file.path)}" name="files" size="30" type="text" value="${file.path}">
59 ${h.dropdownmenu('mimetypes' ,'plain',[('plain',_('plain'))],enable_filter=True, id='mimetype_'+h.FID('f',file.path))}
60 <input type="hidden" name="filename_org" value="${file.path}" >
61 <input id="filename_${h.FID('f',file.path)}" name="filename" size="30" type="text" value="${file.path}">
62 ${h.dropdownmenu('mimetype' ,'plain',[('plain',_('plain'))],enable_filter=True, id='mimetype_'+h.FID('f',file.path))}
60 63 </div>
61 64 </div>
62 65 </div>
63 66 <div class="editor_container">
64 67 <pre id="editor_pre"></pre>
65 <textarea id="editor_${h.FID('f',file.path)}" name="contents" >${file.content}</textarea>
68 <textarea id="editor_${h.FID('f',file.path)}" name="content" >${file.content}</textarea>
66 69 </div>
67 70 </div>
71 <input type="hidden" name="__end__" />
68 72
69 73 ## dynamic edit box.
70 74 <script type="text/javascript">
71 75 $(document).ready(function(){
72 76 var myCodeMirror = initCodeMirror(
73 77 "editor_${h.FID('f',file.path)}", '');
74 78
75 var modes_select = $('#mimetype_${h.FID('f',file.path)}');
79 var modes_select = $("#mimetype_${h.FID('f',file.path)}");
76 80 fillCodeMirrorOptions(modes_select);
77 81
78 82 // try to detect the mode based on the file we edit
79 83 var mimetype = "${file.mimetype}";
80 84 var detected_mode = detectCodeMirrorMode(
81 85 "${file.path}", mimetype);
82 86
83 87 if(detected_mode){
84 88 $(modes_select).select2("val", mimetype);
85 89 $(modes_select).change();
86 90 setCodeMirrorMode(myCodeMirror, detected_mode);
87 91 }
88 92
89 var filename_selector = '#filename_${h.FID('f',file.path)}';
93 var filename_selector = "#filename_${h.FID('f',file.path)}";
90 94 // on change of select field set mode
91 95 setCodeMirrorModeFromSelect(
92 96 modes_select, filename_selector, myCodeMirror, null);
93 97
94 98 // on entering the new filename set mode, from given extension
95 99 setCodeMirrorModeFromInput(
96 100 modes_select, filename_selector, myCodeMirror, null);
97 101 });
98 102 </script>
99
100 103 %endfor
104 <input type="hidden" name="__end__" />
101 105
102 106 <div class="pull-right">
103 107 ${h.submit('update',_('Update Gist'),class_="btn btn-success")}
104 108 <a class="btn" href="${h.url('gist', gist_id=c.gist.gist_access_id)}">${_('Cancel')}</a>
105 109 </div>
106 110 ${h.end_form()}
107 111 </div>
108 112 </div>
109 113
110 114 </div>
111 115 <script>
112 116 $('#update').on('click', function(e){
113 117 e.preventDefault();
114 118
115 119 // check for newer version.
116 120 $.ajax({
117 121 url: "${h.url('edit_gist_check_revision', gist_id=c.gist.gist_access_id)}",
118 122 data: {
119 123 'revision': '${c.file_last_commit.raw_id}'
120 124 },
121 125 dataType: 'json',
122 126 type: 'GET',
123 127 success: function(data) {
124 128 if(data.success === false){
125 129 $('#edit_error').show();
126 130 window.scrollTo(0,0);
127 131 }
128 132 else{
129 133 $('#eform').submit();
130 134 }
131 135 }
132 136 });
133 137 })
134 138
135 139 </script>
136 140 </%def>
@@ -1,86 +1,86 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.html"/>
3 3
4 4 <%def name="title()">
5 5 ${_('New Gist')}
6 6 %if c.rhodecode_name:
7 7 &middot; ${h.branding(c.rhodecode_name)}
8 8 %endif
9 9 </%def>
10 10
11 11 <%def name="breadcrumbs_links()">
12 12 ${_('New Gist')}
13 13 </%def>
14 14
15 15 <%def name="menu_bar_nav()">
16 16 ${self.menu_items(active='gists')}
17 17 </%def>
18 18
19 19 <%def name="main()">
20 20 <div class="box">
21 21 <!-- box / title -->
22 22 <div class="title">
23 23 ${self.breadcrumbs()}
24 24 </div>
25 25
26 26 <div class="table">
27 27 <div id="files_data">
28 28 ${h.secure_form(h.url('gists'), method='post',id='eform')}
29 29 <div>
30 30 <textarea id="description" name="description" placeholder="${_('Gist description ...')}"></textarea>
31 31
32 32 <span class="gist-gravatar">
33 33 ${self.gravatar(c.rhodecode_user.email, 30)}
34 34 </span>
35 35 <label for='gistid'>${_('Gist id')}</label>
36 36 ${h.text('gistid', placeholder=_('Auto generated'))}
37 37
38 38 <label for='lifetime'>${_('Gist lifetime')}</label>
39 39 ${h.dropdownmenu('lifetime', '', c.lifetime_options)}
40 40
41 41 <label for='acl_level'>${_('Gist access level')}</label>
42 ${h.dropdownmenu('acl_level', '', c.acl_options)}
42 ${h.dropdownmenu('gist_acl_level', '', c.acl_options)}
43 43
44 44 </div>
45 45 <div id="codeblock" class="codeblock">
46 46 <div class="code-header">
47 47 <div class="form">
48 48 <div class="fields">
49 49 ${h.text('filename', size=30, placeholder=_('name this file...'))}
50 50 ${h.dropdownmenu('mimetype','plain',[('plain',_('plain'))],enable_filter=True)}
51 51 </div>
52 52 </div>
53 53 </div>
54 54 <div id="editor_container">
55 55 <div id="editor_pre"></div>
56 56 <textarea id="editor" name="content" ></textarea>
57 57 </div>
58 58 </div>
59 59 <div class="pull-right">
60 60 ${h.submit('private',_('Create Private Gist'),class_="btn")}
61 61 ${h.submit('public',_('Create Public Gist'),class_="btn")}
62 62 ${h.reset('reset',_('Reset'),class_="btn")}
63 63 </div>
64 64 ${h.end_form()}
65 65 </div>
66 66 </div>
67 67
68 68 </div>
69 69
70 70 <script type="text/javascript">
71 71 var myCodeMirror = initCodeMirror('editor', '');
72 72
73 73 var modes_select = $('#mimetype');
74 74 fillCodeMirrorOptions(modes_select);
75 75
76 76 var filename_selector = '#filename';
77 77 // on change of select field set mode
78 78 setCodeMirrorModeFromSelect(
79 79 modes_select, filename_selector, myCodeMirror, null);
80 80
81 81 // on entering the new filename set mode, from given extension
82 82 setCodeMirrorModeFromInput(
83 83 modes_select, filename_selector, myCodeMirror, null);
84 84
85 85 </script>
86 86 </%def>
@@ -1,363 +1,359 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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.lib import helpers as h
25 25 from rhodecode.model.db import User, Gist
26 26 from rhodecode.model.gist import GistModel
27 27 from rhodecode.model.meta import Session
28 28 from rhodecode.tests import (
29 29 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
30 30 TestController, assert_session_flash, url)
31 31 from rhodecode.tests.utils import AssertResponse
32 32
33 33
34 34 class GistUtility(object):
35 35
36 36 def __init__(self):
37 37 self._gist_ids = []
38 38
39 39 def __call__(
40 40 self, f_name, content='some gist', lifetime=-1,
41 41 description='gist-desc', gist_type='public',
42 42 acl_level=Gist.GIST_PUBLIC, owner=TEST_USER_ADMIN_LOGIN):
43 43 gist_mapping = {
44 44 f_name: {'content': content}
45 45 }
46 46 user = User.get_by_username(owner)
47 47 gist = GistModel().create(
48 48 description, owner=user, gist_mapping=gist_mapping,
49 49 gist_type=gist_type, lifetime=lifetime, gist_acl_level=acl_level)
50 50 Session().commit()
51 51 self._gist_ids.append(gist.gist_id)
52 52 return gist
53 53
54 54 def cleanup(self):
55 55 for gist_id in self._gist_ids:
56 56 gist = Gist.get(gist_id)
57 57 if gist:
58 58 Session().delete(gist)
59 59
60 60 Session().commit()
61 61
62 62
63 63 @pytest.fixture
64 64 def create_gist(request):
65 65 gist_utility = GistUtility()
66 66 request.addfinalizer(gist_utility.cleanup)
67 67 return gist_utility
68 68
69 69
70 70 class TestGistsController(TestController):
71 71
72 72 def test_index_empty(self, create_gist):
73 73 self.log_user()
74 74 response = self.app.get(url('gists'))
75 75 response.mustcontain('data: [],')
76 76
77 77 def test_index(self, create_gist):
78 78 self.log_user()
79 79 g1 = create_gist('gist1')
80 80 g2 = create_gist('gist2', lifetime=1400)
81 81 g3 = create_gist('gist3', description='gist3-desc')
82 82 g4 = create_gist('gist4', gist_type='private').gist_access_id
83 83 response = self.app.get(url('gists'))
84 84
85 85 response.mustcontain('gist: %s' % g1.gist_access_id)
86 86 response.mustcontain('gist: %s' % g2.gist_access_id)
87 87 response.mustcontain('gist: %s' % g3.gist_access_id)
88 88 response.mustcontain('gist3-desc')
89 89 response.mustcontain(no=['gist: %s' % g4])
90 90
91 91 # Expiration information should be visible
92 92 expires_tag = '%s' % h.age_component(
93 93 h.time_to_datetime(g2.gist_expires))
94 94 response.mustcontain(expires_tag.replace('"', '\\"'))
95 95
96 96 def test_index_private_gists(self, create_gist):
97 97 self.log_user()
98 98 gist = create_gist('gist5', gist_type='private')
99 99 response = self.app.get(url('gists', private=1))
100 100
101 101 # and privates
102 102 response.mustcontain('gist: %s' % gist.gist_access_id)
103 103
104 104 def test_index_show_all(self, create_gist):
105 105 self.log_user()
106 106 create_gist('gist1')
107 107 create_gist('gist2', lifetime=1400)
108 108 create_gist('gist3', description='gist3-desc')
109 109 create_gist('gist4', gist_type='private')
110 110
111 111 response = self.app.get(url('gists', all=1))
112 112
113 113 assert len(GistModel.get_all()) == 4
114 114 # and privates
115 115 for gist in GistModel.get_all():
116 116 response.mustcontain('gist: %s' % gist.gist_access_id)
117 117
118 118 def test_index_show_all_hidden_from_regular(self, create_gist):
119 119 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
120 120 create_gist('gist2', gist_type='private')
121 121 create_gist('gist3', gist_type='private')
122 122 create_gist('gist4', gist_type='private')
123 123
124 124 response = self.app.get(url('gists', all=1))
125 125
126 126 assert len(GistModel.get_all()) == 3
127 127 # since we don't have access to private in this view, we
128 128 # should see nothing
129 129 for gist in GistModel.get_all():
130 130 response.mustcontain(no=['gist: %s' % gist.gist_access_id])
131 131
132 def test_create_missing_description(self):
132 def test_create(self):
133 133 self.log_user()
134 134 response = self.app.post(
135 135 url('gists'),
136 params={'lifetime': -1, 'csrf_token': self.csrf_token},
137 status=200)
138
139 response.mustcontain('Missing value')
140
141 def test_create(self):
142 self.log_user()
143 response = self.app.post(url('gists'),
144 136 params={'lifetime': -1,
145 137 'content': 'gist test',
146 138 'filename': 'foo',
147 139 'public': 'public',
148 'acl_level': Gist.ACL_LEVEL_PUBLIC,
140 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
149 141 'csrf_token': self.csrf_token},
150 142 status=302)
151 143 response = response.follow()
152 144 response.mustcontain('added file: foo')
153 145 response.mustcontain('gist test')
154 146
155 147 def test_create_with_path_with_dirs(self):
156 148 self.log_user()
157 response = self.app.post(url('gists'),
149 response = self.app.post(
150 url('gists'),
158 151 params={'lifetime': -1,
159 152 'content': 'gist test',
160 153 'filename': '/home/foo',
161 154 'public': 'public',
162 'acl_level': Gist.ACL_LEVEL_PUBLIC,
155 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
163 156 'csrf_token': self.csrf_token},
164 157 status=200)
165 response.mustcontain('Filename cannot be inside a directory')
158 response.mustcontain('Filename /home/foo cannot be inside a directory')
166 159
167 160 def test_access_expired_gist(self, create_gist):
168 161 self.log_user()
169 162 gist = create_gist('never-see-me')
170 163 gist.gist_expires = 0 # 1970
171 164 Session().add(gist)
172 165 Session().commit()
173 166
174 167 self.app.get(url('gist', gist_id=gist.gist_access_id), status=404)
175 168
176 169 def test_create_private(self):
177 170 self.log_user()
178 response = self.app.post(url('gists'),
171 response = self.app.post(
172 url('gists'),
179 173 params={'lifetime': -1,
180 174 'content': 'private gist test',
181 175 'filename': 'private-foo',
182 176 'private': 'private',
183 'acl_level': Gist.ACL_LEVEL_PUBLIC,
177 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
184 178 'csrf_token': self.csrf_token},
185 179 status=302)
186 180 response = response.follow()
187 181 response.mustcontain('added file: private-foo<')
188 182 response.mustcontain('private gist test')
189 183 response.mustcontain('Private Gist')
190 184 # Make sure private gists are not indexed by robots
191 185 response.mustcontain(
192 186 '<meta name="robots" content="noindex, nofollow">')
193 187
194 188 def test_create_private_acl_private(self):
195 189 self.log_user()
196 response = self.app.post(url('gists'),
190 response = self.app.post(
191 url('gists'),
197 192 params={'lifetime': -1,
198 193 'content': 'private gist test',
199 194 'filename': 'private-foo',
200 195 'private': 'private',
201 'acl_level': Gist.ACL_LEVEL_PRIVATE,
196 'gist_acl_level': Gist.ACL_LEVEL_PRIVATE,
202 197 'csrf_token': self.csrf_token},
203 198 status=302)
204 199 response = response.follow()
205 200 response.mustcontain('added file: private-foo<')
206 201 response.mustcontain('private gist test')
207 202 response.mustcontain('Private Gist')
208 203 # Make sure private gists are not indexed by robots
209 204 response.mustcontain(
210 205 '<meta name="robots" content="noindex, nofollow">')
211 206
212 207 def test_create_with_description(self):
213 208 self.log_user()
214 response = self.app.post(url('gists'),
209 response = self.app.post(
210 url('gists'),
215 211 params={'lifetime': -1,
216 212 'content': 'gist test',
217 213 'filename': 'foo-desc',
218 214 'description': 'gist-desc',
219 215 'public': 'public',
220 'acl_level': Gist.ACL_LEVEL_PUBLIC,
216 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
221 217 'csrf_token': self.csrf_token},
222 218 status=302)
223 219 response = response.follow()
224 220 response.mustcontain('added file: foo-desc')
225 221 response.mustcontain('gist test')
226 222 response.mustcontain('gist-desc')
227 223
228 224 def test_create_public_with_anonymous_access(self):
229 225 self.log_user()
230 226 params = {
231 227 'lifetime': -1,
232 228 'content': 'gist test',
233 229 'filename': 'foo-desc',
234 230 'description': 'gist-desc',
235 231 'public': 'public',
236 'acl_level': Gist.ACL_LEVEL_PUBLIC,
232 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
237 233 'csrf_token': self.csrf_token
238 234 }
239 235 response = self.app.post(url('gists'), params=params, status=302)
240 236 self.logout_user()
241 237 response = response.follow()
242 238 response.mustcontain('added file: foo-desc')
243 239 response.mustcontain('gist test')
244 240 response.mustcontain('gist-desc')
245 241
246 242 def test_new(self):
247 243 self.log_user()
248 244 self.app.get(url('new_gist'))
249 245
250 246 def test_delete(self, create_gist):
251 247 self.log_user()
252 248 gist = create_gist('delete-me')
253 249 response = self.app.post(
254 250 url('gist', gist_id=gist.gist_id),
255 251 params={'_method': 'delete', 'csrf_token': self.csrf_token})
256 252 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
257 253
258 254 def test_delete_normal_user_his_gist(self, create_gist):
259 255 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
260 256 gist = create_gist('delete-me', owner=TEST_USER_REGULAR_LOGIN)
261 257 response = self.app.post(
262 258 url('gist', gist_id=gist.gist_id),
263 259 params={'_method': 'delete', 'csrf_token': self.csrf_token})
264 260 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
265 261
266 262 def test_delete_normal_user_not_his_own_gist(self, create_gist):
267 263 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
268 264 gist = create_gist('delete-me')
269 265 self.app.post(
270 266 url('gist', gist_id=gist.gist_id),
271 267 params={'_method': 'delete', 'csrf_token': self.csrf_token},
272 268 status=403)
273 269
274 270 def test_show(self, create_gist):
275 271 gist = create_gist('gist-show-me')
276 272 response = self.app.get(url('gist', gist_id=gist.gist_access_id))
277 273
278 274 response.mustcontain('added file: gist-show-me<')
279 275
280 276 assert_response = AssertResponse(response)
281 277 assert_response.element_equals_to(
282 278 'div.rc-user span.user',
283 279 '<span class="user"> %s</span>' % h.link_to_user('test_admin'))
284 280
285 281 response.mustcontain('gist-desc')
286 282
287 283 def test_show_without_hg(self, create_gist):
288 284 with mock.patch(
289 285 'rhodecode.lib.vcs.settings.ALIASES', ['git']):
290 286 gist = create_gist('gist-show-me-again')
291 287 self.app.get(url('gist', gist_id=gist.gist_access_id), status=200)
292 288
293 289 def test_show_acl_private(self, create_gist):
294 290 gist = create_gist('gist-show-me-only-when-im-logged-in',
295 291 acl_level=Gist.ACL_LEVEL_PRIVATE)
296 292 self.app.get(url('gist', gist_id=gist.gist_access_id), status=404)
297 293
298 294 # now we log-in we should see thi gist
299 295 self.log_user()
300 296 response = self.app.get(url('gist', gist_id=gist.gist_access_id))
301 297 response.mustcontain('added file: gist-show-me-only-when-im-logged-in')
302 298
303 299 assert_response = AssertResponse(response)
304 300 assert_response.element_equals_to(
305 301 'div.rc-user span.user',
306 302 '<span class="user"> %s</span>' % h.link_to_user('test_admin'))
307 303 response.mustcontain('gist-desc')
308 304
309 305 def test_show_as_raw(self, create_gist):
310 306 gist = create_gist('gist-show-me', content='GIST CONTENT')
311 307 response = self.app.get(url('formatted_gist',
312 308 gist_id=gist.gist_access_id, format='raw'))
313 309 assert response.body == 'GIST CONTENT'
314 310
315 311 def test_show_as_raw_individual_file(self, create_gist):
316 312 gist = create_gist('gist-show-me-raw', content='GIST BODY')
317 313 response = self.app.get(url('formatted_gist_file',
318 314 gist_id=gist.gist_access_id, format='raw',
319 315 revision='tip', f_path='gist-show-me-raw'))
320 316 assert response.body == 'GIST BODY'
321 317
322 318 def test_edit_page(self, create_gist):
323 319 self.log_user()
324 320 gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
325 321 response = self.app.get(url('edit_gist', gist_id=gist.gist_access_id))
326 322 response.mustcontain('GIST EDIT BODY')
327 323
328 324 def test_edit_page_non_logged_user(self, create_gist):
329 325 gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
330 326 self.app.get(url('edit_gist', gist_id=gist.gist_access_id), status=302)
331 327
332 328 def test_edit_normal_user_his_gist(self, create_gist):
333 329 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
334 330 gist = create_gist('gist-for-edit', owner=TEST_USER_REGULAR_LOGIN)
335 331 self.app.get(url('edit_gist', gist_id=gist.gist_access_id, status=200))
336 332
337 333 def test_edit_normal_user_not_his_own_gist(self, create_gist):
338 334 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
339 335 gist = create_gist('delete-me')
340 336 self.app.get(url('edit_gist', gist_id=gist.gist_access_id), status=403)
341 337
342 338 def test_user_first_name_is_escaped(self, user_util, create_gist):
343 339 xss_atack_string = '"><script>alert(\'First Name\')</script>'
344 340 xss_escaped_string = (
345 341 '&#34;&gt;&lt;script&gt;alert(&#39;First Name&#39;)&lt;/script'
346 342 '&gt;')
347 343 password = 'test'
348 344 user = user_util.create_user(
349 345 firstname=xss_atack_string, password=password)
350 346 create_gist('gist', gist_type='public', owner=user.username)
351 347 response = self.app.get(url('gists'))
352 348 response.mustcontain(xss_escaped_string)
353 349
354 350 def test_user_last_name_is_escaped(self, user_util, create_gist):
355 351 xss_atack_string = '"><script>alert(\'Last Name\')</script>'
356 352 xss_escaped_string = (
357 353 '&#34;&gt;&lt;script&gt;alert(&#39;Last Name&#39;)&lt;/script&gt;')
358 354 password = 'test'
359 355 user = user_util.create_user(
360 356 lastname=xss_atack_string, password=password)
361 357 create_gist('gist', gist_type='public', owner=user.username)
362 358 response = self.app.get(url('gists'))
363 359 response.mustcontain(xss_escaped_string)
@@ -1,46 +1,46 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2016 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 from rhodecode.model.validation_schema import GroupNameType
24 from rhodecode.model.validation_schema.types import GroupNameType
25 25
26 26
27 27 class TestGroupNameType(object):
28 28 @pytest.mark.parametrize('given, expected', [
29 29 ('//group1/group2//', 'group1/group2'),
30 30 ('//group1///group2//', 'group1/group2'),
31 31 ('group1/group2///group3', 'group1/group2/group3')
32 32 ])
33 33 def test_replace_extra_slashes_cleans_up_extra_slashes(
34 34 self, given, expected):
35 35 type_ = GroupNameType()
36 36 result = type_._replace_extra_slashes(given)
37 37 assert result == expected
38 38
39 39 def test_deserialize_cleans_up_extra_slashes(self):
40 40 class TestSchema(colander.Schema):
41 41 field = colander.SchemaNode(GroupNameType())
42 42
43 43 schema = TestSchema()
44 44 cleaned_data = schema.deserialize(
45 45 {'field': '//group1/group2///group3//'})
46 46 assert cleaned_data['field'] == 'group1/group2/group3'
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now