##// 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
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 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2016 RhodeCode GmbH
3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import inspect
21 import inspect
22 import itertools
22 import itertools
23 import logging
23 import logging
24 import types
24 import types
25
25
26 import decorator
26 import decorator
27 import venusian
27 import venusian
28 from pyramid.exceptions import ConfigurationError
28 from pyramid.exceptions import ConfigurationError
29 from pyramid.renderers import render
29 from pyramid.renderers import render
30 from pyramid.response import Response
30 from pyramid.response import Response
31 from pyramid.httpexceptions import HTTPNotFound
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 from rhodecode.lib.auth import AuthUser
35 from rhodecode.lib.auth import AuthUser
35 from rhodecode.lib.base import get_ip_addr
36 from rhodecode.lib.base import get_ip_addr
36 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.utils2 import safe_str
38 from rhodecode.lib.utils2 import safe_str
38 from rhodecode.lib.plugins.utils import get_plugin_settings
39 from rhodecode.lib.plugins.utils import get_plugin_settings
39 from rhodecode.model.db import User, UserApiKeys
40 from rhodecode.model.db import User, UserApiKeys
40
41
41 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
42
43
43 DEFAULT_RENDERER = 'jsonrpc_renderer'
44 DEFAULT_RENDERER = 'jsonrpc_renderer'
44 DEFAULT_URL = '/_admin/apiv2'
45 DEFAULT_URL = '/_admin/apiv2'
45
46
46
47
47 class ExtJsonRenderer(object):
48 class ExtJsonRenderer(object):
48 """
49 """
49 Custom renderer that mkaes use of our ext_json lib
50 Custom renderer that mkaes use of our ext_json lib
50
51
51 """
52 """
52
53
53 def __init__(self, serializer=json.dumps, **kw):
54 def __init__(self, serializer=json.dumps, **kw):
54 """ Any keyword arguments will be passed to the ``serializer``
55 """ Any keyword arguments will be passed to the ``serializer``
55 function."""
56 function."""
56 self.serializer = serializer
57 self.serializer = serializer
57 self.kw = kw
58 self.kw = kw
58
59
59 def __call__(self, info):
60 def __call__(self, info):
60 """ Returns a plain JSON-encoded string with content-type
61 """ Returns a plain JSON-encoded string with content-type
61 ``application/json``. The content-type may be overridden by
62 ``application/json``. The content-type may be overridden by
62 setting ``request.response.content_type``."""
63 setting ``request.response.content_type``."""
63
64
64 def _render(value, system):
65 def _render(value, system):
65 request = system.get('request')
66 request = system.get('request')
66 if request is not None:
67 if request is not None:
67 response = request.response
68 response = request.response
68 ct = response.content_type
69 ct = response.content_type
69 if ct == response.default_content_type:
70 if ct == response.default_content_type:
70 response.content_type = 'application/json'
71 response.content_type = 'application/json'
71
72
72 return self.serializer(value, **self.kw)
73 return self.serializer(value, **self.kw)
73
74
74 return _render
75 return _render
75
76
76
77
77 def jsonrpc_response(request, result):
78 def jsonrpc_response(request, result):
78 rpc_id = getattr(request, 'rpc_id', None)
79 rpc_id = getattr(request, 'rpc_id', None)
79 response = request.response
80 response = request.response
80
81
81 # store content_type before render is called
82 # store content_type before render is called
82 ct = response.content_type
83 ct = response.content_type
83
84
84 ret_value = ''
85 ret_value = ''
85 if rpc_id:
86 if rpc_id:
86 ret_value = {
87 ret_value = {
87 'id': rpc_id,
88 'id': rpc_id,
88 'result': result,
89 'result': result,
89 'error': None,
90 'error': None,
90 }
91 }
91
92
92 # fetch deprecation warnings, and store it inside results
93 # fetch deprecation warnings, and store it inside results
93 deprecation = getattr(request, 'rpc_deprecation', None)
94 deprecation = getattr(request, 'rpc_deprecation', None)
94 if deprecation:
95 if deprecation:
95 ret_value['DEPRECATION_WARNING'] = deprecation
96 ret_value['DEPRECATION_WARNING'] = deprecation
96
97
97 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
98 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
98 response.body = safe_str(raw_body, response.charset)
99 response.body = safe_str(raw_body, response.charset)
99
100
100 if ct == response.default_content_type:
101 if ct == response.default_content_type:
101 response.content_type = 'application/json'
102 response.content_type = 'application/json'
102
103
103 return response
104 return response
104
105
105
106
106 def jsonrpc_error(request, message, retid=None, code=None):
107 def jsonrpc_error(request, message, retid=None, code=None):
107 """
108 """
108 Generate a Response object with a JSON-RPC error body
109 Generate a Response object with a JSON-RPC error body
109
110
110 :param code:
111 :param code:
111 :param retid:
112 :param retid:
112 :param message:
113 :param message:
113 """
114 """
114 err_dict = {'id': retid, 'result': None, 'error': message}
115 err_dict = {'id': retid, 'result': None, 'error': message}
115 body = render(DEFAULT_RENDERER, err_dict, request=request).encode('utf-8')
116 body = render(DEFAULT_RENDERER, err_dict, request=request).encode('utf-8')
116 return Response(
117 return Response(
117 body=body,
118 body=body,
118 status=code,
119 status=code,
119 content_type='application/json'
120 content_type='application/json'
120 )
121 )
121
122
122
123
123 def exception_view(exc, request):
124 def exception_view(exc, request):
124 rpc_id = getattr(request, 'rpc_id', None)
125 rpc_id = getattr(request, 'rpc_id', None)
125
126
126 fault_message = 'undefined error'
127 fault_message = 'undefined error'
127 if isinstance(exc, JSONRPCError):
128 if isinstance(exc, JSONRPCError):
128 fault_message = exc.message
129 fault_message = exc.message
129 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
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 elif isinstance(exc, JSONRPCForbidden):
136 elif isinstance(exc, JSONRPCForbidden):
131 fault_message = 'Access was denied to this resource.'
137 fault_message = 'Access was denied to this resource.'
132 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
138 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
133 elif isinstance(exc, HTTPNotFound):
139 elif isinstance(exc, HTTPNotFound):
134 method = request.rpc_method
140 method = request.rpc_method
135 log.debug('json-rpc method `%s` not found in list of '
141 log.debug('json-rpc method `%s` not found in list of '
136 'api calls: %s, rpc_id:%s',
142 'api calls: %s, rpc_id:%s',
137 method, request.registry.jsonrpc_methods.keys(), rpc_id)
143 method, request.registry.jsonrpc_methods.keys(), rpc_id)
138 fault_message = "No such method: {}".format(method)
144 fault_message = "No such method: {}".format(method)
139
145
140 return jsonrpc_error(request, fault_message, rpc_id)
146 return jsonrpc_error(request, fault_message, rpc_id)
141
147
142
148
143 def request_view(request):
149 def request_view(request):
144 """
150 """
145 Main request handling method. It handles all logic to call a specific
151 Main request handling method. It handles all logic to call a specific
146 exposed method
152 exposed method
147 """
153 """
148
154
149 # check if we can find this session using api_key, get_by_auth_token
155 # check if we can find this session using api_key, get_by_auth_token
150 # search not expired tokens only
156 # search not expired tokens only
151
157
152 try:
158 try:
153 u = User.get_by_auth_token(request.rpc_api_key)
159 u = User.get_by_auth_token(request.rpc_api_key)
154
160
155 if u is None:
161 if u is None:
156 return jsonrpc_error(
162 return jsonrpc_error(
157 request, retid=request.rpc_id, message='Invalid API KEY')
163 request, retid=request.rpc_id, message='Invalid API KEY')
158
164
159 if not u.active:
165 if not u.active:
160 return jsonrpc_error(
166 return jsonrpc_error(
161 request, retid=request.rpc_id,
167 request, retid=request.rpc_id,
162 message='Request from this user not allowed')
168 message='Request from this user not allowed')
163
169
164 # check if we are allowed to use this IP
170 # check if we are allowed to use this IP
165 auth_u = AuthUser(
171 auth_u = AuthUser(
166 u.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
172 u.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
167 if not auth_u.ip_allowed:
173 if not auth_u.ip_allowed:
168 return jsonrpc_error(
174 return jsonrpc_error(
169 request, retid=request.rpc_id,
175 request, retid=request.rpc_id,
170 message='Request from IP:%s not allowed' % (
176 message='Request from IP:%s not allowed' % (
171 request.rpc_ip_addr,))
177 request.rpc_ip_addr,))
172 else:
178 else:
173 log.info('Access for IP:%s allowed' % (request.rpc_ip_addr,))
179 log.info('Access for IP:%s allowed' % (request.rpc_ip_addr,))
174
180
175 # now check if token is valid for API
181 # now check if token is valid for API
176 role = UserApiKeys.ROLE_API
182 role = UserApiKeys.ROLE_API
177 extra_auth_tokens = [
183 extra_auth_tokens = [
178 x.api_key for x in User.extra_valid_auth_tokens(u, role=role)]
184 x.api_key for x in User.extra_valid_auth_tokens(u, role=role)]
179 active_tokens = [u.api_key] + extra_auth_tokens
185 active_tokens = [u.api_key] + extra_auth_tokens
180
186
181 log.debug('Checking if API key has proper role')
187 log.debug('Checking if API key has proper role')
182 if request.rpc_api_key not in active_tokens:
188 if request.rpc_api_key not in active_tokens:
183 return jsonrpc_error(
189 return jsonrpc_error(
184 request, retid=request.rpc_id,
190 request, retid=request.rpc_id,
185 message='API KEY has bad role for an API call')
191 message='API KEY has bad role for an API call')
186
192
187 except Exception as e:
193 except Exception as e:
188 log.exception('Error on API AUTH')
194 log.exception('Error on API AUTH')
189 return jsonrpc_error(
195 return jsonrpc_error(
190 request, retid=request.rpc_id, message='Invalid API KEY')
196 request, retid=request.rpc_id, message='Invalid API KEY')
191
197
192 method = request.rpc_method
198 method = request.rpc_method
193 func = request.registry.jsonrpc_methods[method]
199 func = request.registry.jsonrpc_methods[method]
194
200
195 # now that we have a method, add request._req_params to
201 # now that we have a method, add request._req_params to
196 # self.kargs and dispatch control to WGIController
202 # self.kargs and dispatch control to WGIController
197 argspec = inspect.getargspec(func)
203 argspec = inspect.getargspec(func)
198 arglist = argspec[0]
204 arglist = argspec[0]
199 defaults = map(type, argspec[3] or [])
205 defaults = map(type, argspec[3] or [])
200 default_empty = types.NotImplementedType
206 default_empty = types.NotImplementedType
201
207
202 # kw arguments required by this method
208 # kw arguments required by this method
203 func_kwargs = dict(itertools.izip_longest(
209 func_kwargs = dict(itertools.izip_longest(
204 reversed(arglist), reversed(defaults), fillvalue=default_empty))
210 reversed(arglist), reversed(defaults), fillvalue=default_empty))
205
211
206 # This attribute will need to be first param of a method that uses
212 # This attribute will need to be first param of a method that uses
207 # api_key, which is translated to instance of user at that name
213 # api_key, which is translated to instance of user at that name
208 user_var = 'apiuser'
214 user_var = 'apiuser'
209 request_var = 'request'
215 request_var = 'request'
210
216
211 for arg in [user_var, request_var]:
217 for arg in [user_var, request_var]:
212 if arg not in arglist:
218 if arg not in arglist:
213 return jsonrpc_error(
219 return jsonrpc_error(
214 request,
220 request,
215 retid=request.rpc_id,
221 retid=request.rpc_id,
216 message='This method [%s] does not support '
222 message='This method [%s] does not support '
217 'required parameter `%s`' % (func.__name__, arg))
223 'required parameter `%s`' % (func.__name__, arg))
218
224
219 # get our arglist and check if we provided them as args
225 # get our arglist and check if we provided them as args
220 for arg, default in func_kwargs.items():
226 for arg, default in func_kwargs.items():
221 if arg in [user_var, request_var]:
227 if arg in [user_var, request_var]:
222 # user_var and request_var are pre-hardcoded parameters and we
228 # user_var and request_var are pre-hardcoded parameters and we
223 # don't need to do any translation
229 # don't need to do any translation
224 continue
230 continue
225
231
226 # skip the required param check if it's default value is
232 # skip the required param check if it's default value is
227 # NotImplementedType (default_empty)
233 # NotImplementedType (default_empty)
228 if default == default_empty and arg not in request.rpc_params:
234 if default == default_empty and arg not in request.rpc_params:
229 return jsonrpc_error(
235 return jsonrpc_error(
230 request,
236 request,
231 retid=request.rpc_id,
237 retid=request.rpc_id,
232 message=('Missing non optional `%s` arg in JSON DATA' % arg)
238 message=('Missing non optional `%s` arg in JSON DATA' % arg)
233 )
239 )
234
240
235 # sanitze extra passed arguments
241 # sanitze extra passed arguments
236 for k in request.rpc_params.keys()[:]:
242 for k in request.rpc_params.keys()[:]:
237 if k not in func_kwargs:
243 if k not in func_kwargs:
238 del request.rpc_params[k]
244 del request.rpc_params[k]
239
245
240 call_params = request.rpc_params
246 call_params = request.rpc_params
241 call_params.update({
247 call_params.update({
242 'request': request,
248 'request': request,
243 'apiuser': auth_u
249 'apiuser': auth_u
244 })
250 })
245 try:
251 try:
246 ret_value = func(**call_params)
252 ret_value = func(**call_params)
247 return jsonrpc_response(request, ret_value)
253 return jsonrpc_response(request, ret_value)
248 except JSONRPCBaseError:
254 except JSONRPCBaseError:
249 raise
255 raise
250 except Exception:
256 except Exception:
251 log.exception('Unhandled exception occured on api call: %s', func)
257 log.exception('Unhandled exception occured on api call: %s', func)
252 return jsonrpc_error(request, retid=request.rpc_id,
258 return jsonrpc_error(request, retid=request.rpc_id,
253 message='Internal server error')
259 message='Internal server error')
254
260
255
261
256 def setup_request(request):
262 def setup_request(request):
257 """
263 """
258 Parse a JSON-RPC request body. It's used inside the predicates method
264 Parse a JSON-RPC request body. It's used inside the predicates method
259 to validate and bootstrap requests for usage in rpc calls.
265 to validate and bootstrap requests for usage in rpc calls.
260
266
261 We need to raise JSONRPCError here if we want to return some errors back to
267 We need to raise JSONRPCError here if we want to return some errors back to
262 user.
268 user.
263 """
269 """
264 log.debug('Executing setup request: %r', request)
270 log.debug('Executing setup request: %r', request)
265 request.rpc_ip_addr = get_ip_addr(request.environ)
271 request.rpc_ip_addr = get_ip_addr(request.environ)
266 # TODO: marcink, deprecate GET at some point
272 # TODO: marcink, deprecate GET at some point
267 if request.method not in ['POST', 'GET']:
273 if request.method not in ['POST', 'GET']:
268 log.debug('unsupported request method "%s"', request.method)
274 log.debug('unsupported request method "%s"', request.method)
269 raise JSONRPCError(
275 raise JSONRPCError(
270 'unsupported request method "%s". Please use POST' % request.method)
276 'unsupported request method "%s". Please use POST' % request.method)
271
277
272 if 'CONTENT_LENGTH' not in request.environ:
278 if 'CONTENT_LENGTH' not in request.environ:
273 log.debug("No Content-Length")
279 log.debug("No Content-Length")
274 raise JSONRPCError("Empty body, No Content-Length in request")
280 raise JSONRPCError("Empty body, No Content-Length in request")
275
281
276 else:
282 else:
277 length = request.environ['CONTENT_LENGTH']
283 length = request.environ['CONTENT_LENGTH']
278 log.debug('Content-Length: %s', length)
284 log.debug('Content-Length: %s', length)
279
285
280 if length == 0:
286 if length == 0:
281 log.debug("Content-Length is 0")
287 log.debug("Content-Length is 0")
282 raise JSONRPCError("Content-Length is 0")
288 raise JSONRPCError("Content-Length is 0")
283
289
284 raw_body = request.body
290 raw_body = request.body
285 try:
291 try:
286 json_body = json.loads(raw_body)
292 json_body = json.loads(raw_body)
287 except ValueError as e:
293 except ValueError as e:
288 # catch JSON errors Here
294 # catch JSON errors Here
289 raise JSONRPCError("JSON parse error ERR:%s RAW:%r" % (e, raw_body))
295 raise JSONRPCError("JSON parse error ERR:%s RAW:%r" % (e, raw_body))
290
296
291 request.rpc_id = json_body.get('id')
297 request.rpc_id = json_body.get('id')
292 request.rpc_method = json_body.get('method')
298 request.rpc_method = json_body.get('method')
293
299
294 # check required base parameters
300 # check required base parameters
295 try:
301 try:
296 api_key = json_body.get('api_key')
302 api_key = json_body.get('api_key')
297 if not api_key:
303 if not api_key:
298 api_key = json_body.get('auth_token')
304 api_key = json_body.get('auth_token')
299
305
300 if not api_key:
306 if not api_key:
301 raise KeyError('api_key or auth_token')
307 raise KeyError('api_key or auth_token')
302
308
303 request.rpc_api_key = api_key
309 request.rpc_api_key = api_key
304 request.rpc_id = json_body['id']
310 request.rpc_id = json_body['id']
305 request.rpc_method = json_body['method']
311 request.rpc_method = json_body['method']
306 request.rpc_params = json_body['args'] \
312 request.rpc_params = json_body['args'] \
307 if isinstance(json_body['args'], dict) else {}
313 if isinstance(json_body['args'], dict) else {}
308
314
309 log.debug(
315 log.debug(
310 'method: %s, params: %s' % (request.rpc_method, request.rpc_params))
316 'method: %s, params: %s' % (request.rpc_method, request.rpc_params))
311 except KeyError as e:
317 except KeyError as e:
312 raise JSONRPCError('Incorrect JSON data. Missing %s' % e)
318 raise JSONRPCError('Incorrect JSON data. Missing %s' % e)
313
319
314 log.debug('setup complete, now handling method:%s rpcid:%s',
320 log.debug('setup complete, now handling method:%s rpcid:%s',
315 request.rpc_method, request.rpc_id, )
321 request.rpc_method, request.rpc_id, )
316
322
317
323
318 class RoutePredicate(object):
324 class RoutePredicate(object):
319 def __init__(self, val, config):
325 def __init__(self, val, config):
320 self.val = val
326 self.val = val
321
327
322 def text(self):
328 def text(self):
323 return 'jsonrpc route = %s' % self.val
329 return 'jsonrpc route = %s' % self.val
324
330
325 phash = text
331 phash = text
326
332
327 def __call__(self, info, request):
333 def __call__(self, info, request):
328 if self.val:
334 if self.val:
329 # potentially setup and bootstrap our call
335 # potentially setup and bootstrap our call
330 setup_request(request)
336 setup_request(request)
331
337
332 # Always return True so that even if it isn't a valid RPC it
338 # Always return True so that even if it isn't a valid RPC it
333 # will fall through to the underlaying handlers like notfound_view
339 # will fall through to the underlaying handlers like notfound_view
334 return True
340 return True
335
341
336
342
337 class NotFoundPredicate(object):
343 class NotFoundPredicate(object):
338 def __init__(self, val, config):
344 def __init__(self, val, config):
339 self.val = val
345 self.val = val
340
346
341 def text(self):
347 def text(self):
342 return 'jsonrpc method not found = %s' % self.val
348 return 'jsonrpc method not found = %s' % self.val
343
349
344 phash = text
350 phash = text
345
351
346 def __call__(self, info, request):
352 def __call__(self, info, request):
347 return hasattr(request, 'rpc_method')
353 return hasattr(request, 'rpc_method')
348
354
349
355
350 class MethodPredicate(object):
356 class MethodPredicate(object):
351 def __init__(self, val, config):
357 def __init__(self, val, config):
352 self.method = val
358 self.method = val
353
359
354 def text(self):
360 def text(self):
355 return 'jsonrpc method = %s' % self.method
361 return 'jsonrpc method = %s' % self.method
356
362
357 phash = text
363 phash = text
358
364
359 def __call__(self, context, request):
365 def __call__(self, context, request):
360 # we need to explicitly return False here, so pyramid doesn't try to
366 # we need to explicitly return False here, so pyramid doesn't try to
361 # execute our view directly. We need our main handler to execute things
367 # execute our view directly. We need our main handler to execute things
362 return getattr(request, 'rpc_method') == self.method
368 return getattr(request, 'rpc_method') == self.method
363
369
364
370
365 def add_jsonrpc_method(config, view, **kwargs):
371 def add_jsonrpc_method(config, view, **kwargs):
366 # pop the method name
372 # pop the method name
367 method = kwargs.pop('method', None)
373 method = kwargs.pop('method', None)
368
374
369 if method is None:
375 if method is None:
370 raise ConfigurationError(
376 raise ConfigurationError(
371 'Cannot register a JSON-RPC method without specifying the '
377 'Cannot register a JSON-RPC method without specifying the '
372 '"method"')
378 '"method"')
373
379
374 # we define custom predicate, to enable to detect conflicting methods,
380 # we define custom predicate, to enable to detect conflicting methods,
375 # those predicates are kind of "translation" from the decorator variables
381 # those predicates are kind of "translation" from the decorator variables
376 # to internal predicates names
382 # to internal predicates names
377
383
378 kwargs['jsonrpc_method'] = method
384 kwargs['jsonrpc_method'] = method
379
385
380 # register our view into global view store for validation
386 # register our view into global view store for validation
381 config.registry.jsonrpc_methods[method] = view
387 config.registry.jsonrpc_methods[method] = view
382
388
383 # we're using our main request_view handler, here, so each method
389 # we're using our main request_view handler, here, so each method
384 # has a unified handler for itself
390 # has a unified handler for itself
385 config.add_view(request_view, route_name='apiv2', **kwargs)
391 config.add_view(request_view, route_name='apiv2', **kwargs)
386
392
387
393
388 class jsonrpc_method(object):
394 class jsonrpc_method(object):
389 """
395 """
390 decorator that works similar to @add_view_config decorator,
396 decorator that works similar to @add_view_config decorator,
391 but tailored for our JSON RPC
397 but tailored for our JSON RPC
392 """
398 """
393
399
394 venusian = venusian # for testing injection
400 venusian = venusian # for testing injection
395
401
396 def __init__(self, method=None, **kwargs):
402 def __init__(self, method=None, **kwargs):
397 self.method = method
403 self.method = method
398 self.kwargs = kwargs
404 self.kwargs = kwargs
399
405
400 def __call__(self, wrapped):
406 def __call__(self, wrapped):
401 kwargs = self.kwargs.copy()
407 kwargs = self.kwargs.copy()
402 kwargs['method'] = self.method or wrapped.__name__
408 kwargs['method'] = self.method or wrapped.__name__
403 depth = kwargs.pop('_depth', 0)
409 depth = kwargs.pop('_depth', 0)
404
410
405 def callback(context, name, ob):
411 def callback(context, name, ob):
406 config = context.config.with_package(info.module)
412 config = context.config.with_package(info.module)
407 config.add_jsonrpc_method(view=ob, **kwargs)
413 config.add_jsonrpc_method(view=ob, **kwargs)
408
414
409 info = venusian.attach(wrapped, callback, category='pyramid',
415 info = venusian.attach(wrapped, callback, category='pyramid',
410 depth=depth + 1)
416 depth=depth + 1)
411 if info.scope == 'class':
417 if info.scope == 'class':
412 # ensure that attr is set if decorating a class method
418 # ensure that attr is set if decorating a class method
413 kwargs.setdefault('attr', wrapped.__name__)
419 kwargs.setdefault('attr', wrapped.__name__)
414
420
415 kwargs['_info'] = info.codeinfo # fbo action_method
421 kwargs['_info'] = info.codeinfo # fbo action_method
416 return wrapped
422 return wrapped
417
423
418
424
419 class jsonrpc_deprecated_method(object):
425 class jsonrpc_deprecated_method(object):
420 """
426 """
421 Marks method as deprecated, adds log.warning, and inject special key to
427 Marks method as deprecated, adds log.warning, and inject special key to
422 the request variable to mark method as deprecated.
428 the request variable to mark method as deprecated.
423 Also injects special docstring that extract_docs will catch to mark
429 Also injects special docstring that extract_docs will catch to mark
424 method as deprecated.
430 method as deprecated.
425
431
426 :param use_method: specify which method should be used instead of
432 :param use_method: specify which method should be used instead of
427 the decorated one
433 the decorated one
428
434
429 Use like::
435 Use like::
430
436
431 @jsonrpc_method()
437 @jsonrpc_method()
432 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
438 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
433 def old_func(request, apiuser, arg1, arg2):
439 def old_func(request, apiuser, arg1, arg2):
434 ...
440 ...
435 """
441 """
436
442
437 def __init__(self, use_method, deprecated_at_version):
443 def __init__(self, use_method, deprecated_at_version):
438 self.use_method = use_method
444 self.use_method = use_method
439 self.deprecated_at_version = deprecated_at_version
445 self.deprecated_at_version = deprecated_at_version
440 self.deprecated_msg = ''
446 self.deprecated_msg = ''
441
447
442 def __call__(self, func):
448 def __call__(self, func):
443 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
449 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
444 method=self.use_method)
450 method=self.use_method)
445
451
446 docstring = """\n
452 docstring = """\n
447 .. deprecated:: {version}
453 .. deprecated:: {version}
448
454
449 {deprecation_message}
455 {deprecation_message}
450
456
451 {original_docstring}
457 {original_docstring}
452 """
458 """
453 func.__doc__ = docstring.format(
459 func.__doc__ = docstring.format(
454 version=self.deprecated_at_version,
460 version=self.deprecated_at_version,
455 deprecation_message=self.deprecated_msg,
461 deprecation_message=self.deprecated_msg,
456 original_docstring=func.__doc__)
462 original_docstring=func.__doc__)
457 return decorator.decorator(self.__wrapper, func)
463 return decorator.decorator(self.__wrapper, func)
458
464
459 def __wrapper(self, func, *fargs, **fkwargs):
465 def __wrapper(self, func, *fargs, **fkwargs):
460 log.warning('DEPRECATED API CALL on function %s, please '
466 log.warning('DEPRECATED API CALL on function %s, please '
461 'use `%s` instead', func, self.use_method)
467 'use `%s` instead', func, self.use_method)
462 # alter function docstring to mark as deprecated, this is picked up
468 # alter function docstring to mark as deprecated, this is picked up
463 # via fabric file that generates API DOC.
469 # via fabric file that generates API DOC.
464 result = func(*fargs, **fkwargs)
470 result = func(*fargs, **fkwargs)
465
471
466 request = fargs[0]
472 request = fargs[0]
467 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
473 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
468 return result
474 return result
469
475
470
476
471 def includeme(config):
477 def includeme(config):
472 plugin_module = 'rhodecode.api'
478 plugin_module = 'rhodecode.api'
473 plugin_settings = get_plugin_settings(
479 plugin_settings = get_plugin_settings(
474 plugin_module, config.registry.settings)
480 plugin_module, config.registry.settings)
475
481
476 if not hasattr(config.registry, 'jsonrpc_methods'):
482 if not hasattr(config.registry, 'jsonrpc_methods'):
477 config.registry.jsonrpc_methods = {}
483 config.registry.jsonrpc_methods = {}
478
484
479 # match filter by given method only
485 # match filter by given method only
480 config.add_view_predicate(
486 config.add_view_predicate(
481 'jsonrpc_method', MethodPredicate)
487 'jsonrpc_method', MethodPredicate)
482
488
483 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
489 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
484 serializer=json.dumps, indent=4))
490 serializer=json.dumps, indent=4))
485 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
491 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
486
492
487 config.add_route_predicate(
493 config.add_route_predicate(
488 'jsonrpc_call', RoutePredicate)
494 'jsonrpc_call', RoutePredicate)
489
495
490 config.add_route(
496 config.add_route(
491 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
497 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
492
498
493 config.scan(plugin_module, ignore='rhodecode.api.tests')
499 config.scan(plugin_module, ignore='rhodecode.api.tests')
494 # register some exception handling view
500 # register some exception handling view
495 config.add_view(exception_view, context=JSONRPCBaseError)
501 config.add_view(exception_view, context=JSONRPCBaseError)
496 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
502 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
497 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
503 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
@@ -1,31 +1,39 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2016 RhodeCode GmbH
3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 class JSONRPCBaseError(Exception):
22 class JSONRPCBaseError(Exception):
23 pass
23 pass
24
24
25
25
26 class JSONRPCError(JSONRPCBaseError):
26 class JSONRPCError(JSONRPCBaseError):
27 pass
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 class JSONRPCForbidden(JSONRPCBaseError):
37 class JSONRPCForbidden(JSONRPCBaseError):
31 pass
38 pass
39
@@ -1,76 +1,102 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import mock
21 import mock
22 import pytest
22 import pytest
23
23
24 from rhodecode.model.db import Gist
24 from rhodecode.model.db import Gist
25 from rhodecode.model.gist import GistModel
25 from rhodecode.model.gist import GistModel
26 from rhodecode.api.tests.utils import (
26 from rhodecode.api.tests.utils import (
27 build_data, api_call, assert_error, assert_ok, crash)
27 build_data, api_call, assert_error, assert_ok, crash)
28 from rhodecode.tests.fixture import Fixture
28 from rhodecode.tests.fixture import Fixture
29
29
30
30
31 @pytest.mark.usefixtures("testuser_api", "app")
31 @pytest.mark.usefixtures("testuser_api", "app")
32 class TestApiCreateGist(object):
32 class TestApiCreateGist(object):
33 @pytest.mark.parametrize("lifetime, gist_type, gist_acl_level", [
33 @pytest.mark.parametrize("lifetime, gist_type, gist_acl_level", [
34 (10, Gist.GIST_PUBLIC, Gist.ACL_LEVEL_PUBLIC),
34 (10, Gist.GIST_PUBLIC, Gist.ACL_LEVEL_PUBLIC),
35 (20, Gist.GIST_PUBLIC, Gist.ACL_LEVEL_PRIVATE),
35 (20, Gist.GIST_PUBLIC, Gist.ACL_LEVEL_PRIVATE),
36 (40, Gist.GIST_PRIVATE, Gist.ACL_LEVEL_PUBLIC),
36 (40, Gist.GIST_PRIVATE, Gist.ACL_LEVEL_PUBLIC),
37 (80, Gist.GIST_PRIVATE, Gist.ACL_LEVEL_PRIVATE),
37 (80, Gist.GIST_PRIVATE, Gist.ACL_LEVEL_PRIVATE),
38 ])
38 ])
39 def test_api_create_gist(self, lifetime, gist_type, gist_acl_level):
39 def test_api_create_gist(self, lifetime, gist_type, gist_acl_level):
40 id_, params = build_data(
40 id_, params = build_data(
41 self.apikey_regular, 'create_gist',
41 self.apikey_regular, 'create_gist',
42 lifetime=lifetime,
42 lifetime=lifetime,
43 description='foobar-gist',
43 description='foobar-gist',
44 gist_type=gist_type,
44 gist_type=gist_type,
45 acl_level=gist_acl_level,
45 acl_level=gist_acl_level,
46 files={'foobar': {'content': 'foo'}})
46 files={'foobar_ąć': {'content': 'foo'}})
47 response = api_call(self.app, params)
47 response = api_call(self.app, params)
48 response_json = response.json
48 response_json = response.json
49 gist = response_json['result']['gist']
49 gist = response_json['result']['gist']
50 expected = {
50 expected = {
51 'gist': {
51 'gist': {
52 'access_id': gist['access_id'],
52 'access_id': gist['access_id'],
53 'created_on': gist['created_on'],
53 'created_on': gist['created_on'],
54 'modified_at': gist['modified_at'],
54 'modified_at': gist['modified_at'],
55 'description': 'foobar-gist',
55 'description': 'foobar-gist',
56 'expires': gist['expires'],
56 'expires': gist['expires'],
57 'gist_id': gist['gist_id'],
57 'gist_id': gist['gist_id'],
58 'type': gist_type,
58 'type': gist_type,
59 'url': gist['url'],
59 'url': gist['url'],
60 # content is empty since we don't show it here
60 # content is empty since we don't show it here
61 'content': None,
61 'content': None,
62 'acl_level': gist_acl_level,
62 'acl_level': gist_acl_level,
63 },
63 },
64 'msg': 'created new gist'
64 'msg': 'created new gist'
65 }
65 }
66 try:
66 try:
67 assert_ok(id_, expected, given=response.body)
67 assert_ok(id_, expected, given=response.body)
68 finally:
68 finally:
69 Fixture().destroy_gists()
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 @mock.patch.object(GistModel, 'create', crash)
97 @mock.patch.object(GistModel, 'create', crash)
72 def test_api_create_gist_exception_occurred(self):
98 def test_api_create_gist_exception_occurred(self):
73 id_, params = build_data(self.apikey_regular, 'create_gist', files={})
99 id_, params = build_data(self.apikey_regular, 'create_gist', files={})
74 response = api_call(self.app, params)
100 response = api_call(self.app, params)
75 expected = 'failed to create gist'
101 expected = 'failed to create gist'
76 assert_error(id_, expected, given=response.body)
102 assert_error(id_, expected, given=response.body)
@@ -1,378 +1,376 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2016 RhodeCode GmbH
3 # Copyright (C) 2014-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 JSON RPC utils
22 JSON RPC utils
23 """
23 """
24
24
25 import collections
25 import collections
26 import logging
26 import logging
27
27
28 from rhodecode.api.exc import JSONRPCError
28 from rhodecode.api.exc import JSONRPCError
29 from rhodecode.lib.auth import HasPermissionAnyApi, HasRepoPermissionAnyApi
29 from rhodecode.lib.auth import HasPermissionAnyApi, HasRepoPermissionAnyApi
30 from rhodecode.lib.utils import safe_unicode
30 from rhodecode.lib.utils import safe_unicode
31 from rhodecode.controllers.utils import get_commit_from_ref_name
31 from rhodecode.controllers.utils import get_commit_from_ref_name
32 from rhodecode.lib.vcs.exceptions import RepositoryError
32 from rhodecode.lib.vcs.exceptions import RepositoryError
33
33
34 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
35
35
36
36
37
38
39 class OAttr(object):
37 class OAttr(object):
40 """
38 """
41 Special Option that defines other attribute, and can default to them
39 Special Option that defines other attribute, and can default to them
42
40
43 Example::
41 Example::
44
42
45 def test(apiuser, userid=Optional(OAttr('apiuser')):
43 def test(apiuser, userid=Optional(OAttr('apiuser')):
46 user = Optional.extract(userid, evaluate_locals=local())
44 user = Optional.extract(userid, evaluate_locals=local())
47 #if we pass in userid, we get it, else it will default to apiuser
45 #if we pass in userid, we get it, else it will default to apiuser
48 #attribute
46 #attribute
49 """
47 """
50
48
51 def __init__(self, attr_name):
49 def __init__(self, attr_name):
52 self.attr_name = attr_name
50 self.attr_name = attr_name
53
51
54 def __repr__(self):
52 def __repr__(self):
55 return '<OptionalAttr:%s>' % self.attr_name
53 return '<OptionalAttr:%s>' % self.attr_name
56
54
57 def __call__(self):
55 def __call__(self):
58 return self
56 return self
59
57
60
58
61 class Optional(object):
59 class Optional(object):
62 """
60 """
63 Defines an optional parameter::
61 Defines an optional parameter::
64
62
65 param = param.getval() if isinstance(param, Optional) else param
63 param = param.getval() if isinstance(param, Optional) else param
66 param = param() if isinstance(param, Optional) else param
64 param = param() if isinstance(param, Optional) else param
67
65
68 is equivalent of::
66 is equivalent of::
69
67
70 param = Optional.extract(param)
68 param = Optional.extract(param)
71
69
72 """
70 """
73
71
74 def __init__(self, type_):
72 def __init__(self, type_):
75 self.type_ = type_
73 self.type_ = type_
76
74
77 def __repr__(self):
75 def __repr__(self):
78 return '<Optional:%s>' % self.type_.__repr__()
76 return '<Optional:%s>' % self.type_.__repr__()
79
77
80 def __call__(self):
78 def __call__(self):
81 return self.getval()
79 return self.getval()
82
80
83 def getval(self, evaluate_locals=None):
81 def getval(self, evaluate_locals=None):
84 """
82 """
85 returns value from this Optional instance
83 returns value from this Optional instance
86 """
84 """
87 if isinstance(self.type_, OAttr):
85 if isinstance(self.type_, OAttr):
88 param_name = self.type_.attr_name
86 param_name = self.type_.attr_name
89 if evaluate_locals:
87 if evaluate_locals:
90 return evaluate_locals[param_name]
88 return evaluate_locals[param_name]
91 # use params name
89 # use params name
92 return param_name
90 return param_name
93 return self.type_
91 return self.type_
94
92
95 @classmethod
93 @classmethod
96 def extract(cls, val, evaluate_locals=None):
94 def extract(cls, val, evaluate_locals=None):
97 """
95 """
98 Extracts value from Optional() instance
96 Extracts value from Optional() instance
99
97
100 :param val:
98 :param val:
101 :return: original value if it's not Optional instance else
99 :return: original value if it's not Optional instance else
102 value of instance
100 value of instance
103 """
101 """
104 if isinstance(val, cls):
102 if isinstance(val, cls):
105 return val.getval(evaluate_locals)
103 return val.getval(evaluate_locals)
106 return val
104 return val
107
105
108
106
109 def parse_args(cli_args, key_prefix=''):
107 def parse_args(cli_args, key_prefix=''):
110 from rhodecode.lib.utils2 import (escape_split)
108 from rhodecode.lib.utils2 import (escape_split)
111 kwargs = collections.defaultdict(dict)
109 kwargs = collections.defaultdict(dict)
112 for el in escape_split(cli_args, ','):
110 for el in escape_split(cli_args, ','):
113 kv = escape_split(el, '=', 1)
111 kv = escape_split(el, '=', 1)
114 if len(kv) == 2:
112 if len(kv) == 2:
115 k, v = kv
113 k, v = kv
116 kwargs[key_prefix + k] = v
114 kwargs[key_prefix + k] = v
117 return kwargs
115 return kwargs
118
116
119
117
120 def get_origin(obj):
118 def get_origin(obj):
121 """
119 """
122 Get origin of permission from object.
120 Get origin of permission from object.
123
121
124 :param obj:
122 :param obj:
125 """
123 """
126 origin = 'permission'
124 origin = 'permission'
127
125
128 if getattr(obj, 'owner_row', '') and getattr(obj, 'admin_row', ''):
126 if getattr(obj, 'owner_row', '') and getattr(obj, 'admin_row', ''):
129 # admin and owner case, maybe we should use dual string ?
127 # admin and owner case, maybe we should use dual string ?
130 origin = 'owner'
128 origin = 'owner'
131 elif getattr(obj, 'owner_row', ''):
129 elif getattr(obj, 'owner_row', ''):
132 origin = 'owner'
130 origin = 'owner'
133 elif getattr(obj, 'admin_row', ''):
131 elif getattr(obj, 'admin_row', ''):
134 origin = 'super-admin'
132 origin = 'super-admin'
135 return origin
133 return origin
136
134
137
135
138 def store_update(updates, attr, name):
136 def store_update(updates, attr, name):
139 """
137 """
140 Stores param in updates dict if it's not instance of Optional
138 Stores param in updates dict if it's not instance of Optional
141 allows easy updates of passed in params
139 allows easy updates of passed in params
142 """
140 """
143 if not isinstance(attr, Optional):
141 if not isinstance(attr, Optional):
144 updates[name] = attr
142 updates[name] = attr
145
143
146
144
147 def has_superadmin_permission(apiuser):
145 def has_superadmin_permission(apiuser):
148 """
146 """
149 Return True if apiuser is admin or return False
147 Return True if apiuser is admin or return False
150
148
151 :param apiuser:
149 :param apiuser:
152 """
150 """
153 if HasPermissionAnyApi('hg.admin')(user=apiuser):
151 if HasPermissionAnyApi('hg.admin')(user=apiuser):
154 return True
152 return True
155 return False
153 return False
156
154
157
155
158 def has_repo_permissions(apiuser, repoid, repo, perms):
156 def has_repo_permissions(apiuser, repoid, repo, perms):
159 """
157 """
160 Raise JsonRPCError if apiuser is not authorized or return True
158 Raise JsonRPCError if apiuser is not authorized or return True
161
159
162 :param apiuser:
160 :param apiuser:
163 :param repoid:
161 :param repoid:
164 :param repo:
162 :param repo:
165 :param perms:
163 :param perms:
166 """
164 """
167 if not HasRepoPermissionAnyApi(*perms)(
165 if not HasRepoPermissionAnyApi(*perms)(
168 user=apiuser, repo_name=repo.repo_name):
166 user=apiuser, repo_name=repo.repo_name):
169 raise JSONRPCError(
167 raise JSONRPCError(
170 'repository `%s` does not exist' % repoid)
168 'repository `%s` does not exist' % repoid)
171
169
172 return True
170 return True
173
171
174
172
175 def get_user_or_error(userid):
173 def get_user_or_error(userid):
176 """
174 """
177 Get user by id or name or return JsonRPCError if not found
175 Get user by id or name or return JsonRPCError if not found
178
176
179 :param userid:
177 :param userid:
180 """
178 """
181 from rhodecode.model.user import UserModel
179 from rhodecode.model.user import UserModel
182
180
183 user_model = UserModel()
181 user_model = UserModel()
184 try:
182 try:
185 user = user_model.get_user(int(userid))
183 user = user_model.get_user(int(userid))
186 except ValueError:
184 except ValueError:
187 user = user_model.get_by_username(userid)
185 user = user_model.get_by_username(userid)
188
186
189 if user is None:
187 if user is None:
190 raise JSONRPCError("user `%s` does not exist" % (userid,))
188 raise JSONRPCError("user `%s` does not exist" % (userid,))
191 return user
189 return user
192
190
193
191
194 def get_repo_or_error(repoid):
192 def get_repo_or_error(repoid):
195 """
193 """
196 Get repo by id or name or return JsonRPCError if not found
194 Get repo by id or name or return JsonRPCError if not found
197
195
198 :param repoid:
196 :param repoid:
199 """
197 """
200 from rhodecode.model.repo import RepoModel
198 from rhodecode.model.repo import RepoModel
201
199
202 repo = RepoModel().get_repo(repoid)
200 repo = RepoModel().get_repo(repoid)
203 if repo is None:
201 if repo is None:
204 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
202 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
205 return repo
203 return repo
206
204
207
205
208 def get_repo_group_or_error(repogroupid):
206 def get_repo_group_or_error(repogroupid):
209 """
207 """
210 Get repo group by id or name or return JsonRPCError if not found
208 Get repo group by id or name or return JsonRPCError if not found
211
209
212 :param repogroupid:
210 :param repogroupid:
213 """
211 """
214 from rhodecode.model.repo_group import RepoGroupModel
212 from rhodecode.model.repo_group import RepoGroupModel
215
213
216 repo_group = RepoGroupModel()._get_repo_group(repogroupid)
214 repo_group = RepoGroupModel()._get_repo_group(repogroupid)
217 if repo_group is None:
215 if repo_group is None:
218 raise JSONRPCError(
216 raise JSONRPCError(
219 'repository group `%s` does not exist' % (repogroupid,))
217 'repository group `%s` does not exist' % (repogroupid,))
220 return repo_group
218 return repo_group
221
219
222
220
223 def get_user_group_or_error(usergroupid):
221 def get_user_group_or_error(usergroupid):
224 """
222 """
225 Get user group by id or name or return JsonRPCError if not found
223 Get user group by id or name or return JsonRPCError if not found
226
224
227 :param usergroupid:
225 :param usergroupid:
228 """
226 """
229 from rhodecode.model.user_group import UserGroupModel
227 from rhodecode.model.user_group import UserGroupModel
230
228
231 user_group = UserGroupModel().get_group(usergroupid)
229 user_group = UserGroupModel().get_group(usergroupid)
232 if user_group is None:
230 if user_group is None:
233 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
231 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
234 return user_group
232 return user_group
235
233
236
234
237 def get_perm_or_error(permid, prefix=None):
235 def get_perm_or_error(permid, prefix=None):
238 """
236 """
239 Get permission by id or name or return JsonRPCError if not found
237 Get permission by id or name or return JsonRPCError if not found
240
238
241 :param permid:
239 :param permid:
242 """
240 """
243 from rhodecode.model.permission import PermissionModel
241 from rhodecode.model.permission import PermissionModel
244
242
245 perm = PermissionModel.cls.get_by_key(permid)
243 perm = PermissionModel.cls.get_by_key(permid)
246 if perm is None:
244 if perm is None:
247 raise JSONRPCError('permission `%s` does not exist' % (permid,))
245 raise JSONRPCError('permission `%s` does not exist' % (permid,))
248 if prefix:
246 if prefix:
249 if not perm.permission_name.startswith(prefix):
247 if not perm.permission_name.startswith(prefix):
250 raise JSONRPCError('permission `%s` is invalid, '
248 raise JSONRPCError('permission `%s` is invalid, '
251 'should start with %s' % (permid, prefix))
249 'should start with %s' % (permid, prefix))
252 return perm
250 return perm
253
251
254
252
255 def get_gist_or_error(gistid):
253 def get_gist_or_error(gistid):
256 """
254 """
257 Get gist by id or gist_access_id or return JsonRPCError if not found
255 Get gist by id or gist_access_id or return JsonRPCError if not found
258
256
259 :param gistid:
257 :param gistid:
260 """
258 """
261 from rhodecode.model.gist import GistModel
259 from rhodecode.model.gist import GistModel
262
260
263 gist = GistModel.cls.get_by_access_id(gistid)
261 gist = GistModel.cls.get_by_access_id(gistid)
264 if gist is None:
262 if gist is None:
265 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
263 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
266 return gist
264 return gist
267
265
268
266
269 def get_pull_request_or_error(pullrequestid):
267 def get_pull_request_or_error(pullrequestid):
270 """
268 """
271 Get pull request by id or return JsonRPCError if not found
269 Get pull request by id or return JsonRPCError if not found
272
270
273 :param pullrequestid:
271 :param pullrequestid:
274 """
272 """
275 from rhodecode.model.pull_request import PullRequestModel
273 from rhodecode.model.pull_request import PullRequestModel
276
274
277 try:
275 try:
278 pull_request = PullRequestModel().get(int(pullrequestid))
276 pull_request = PullRequestModel().get(int(pullrequestid))
279 except ValueError:
277 except ValueError:
280 raise JSONRPCError('pullrequestid must be an integer')
278 raise JSONRPCError('pullrequestid must be an integer')
281 if not pull_request:
279 if not pull_request:
282 raise JSONRPCError('pull request `%s` does not exist' % (
280 raise JSONRPCError('pull request `%s` does not exist' % (
283 pullrequestid,))
281 pullrequestid,))
284 return pull_request
282 return pull_request
285
283
286
284
287 def build_commit_data(commit, detail_level):
285 def build_commit_data(commit, detail_level):
288 parsed_diff = []
286 parsed_diff = []
289 if detail_level == 'extended':
287 if detail_level == 'extended':
290 for f in commit.added:
288 for f in commit.added:
291 parsed_diff.append(_get_commit_dict(filename=f.path, op='A'))
289 parsed_diff.append(_get_commit_dict(filename=f.path, op='A'))
292 for f in commit.changed:
290 for f in commit.changed:
293 parsed_diff.append(_get_commit_dict(filename=f.path, op='M'))
291 parsed_diff.append(_get_commit_dict(filename=f.path, op='M'))
294 for f in commit.removed:
292 for f in commit.removed:
295 parsed_diff.append(_get_commit_dict(filename=f.path, op='D'))
293 parsed_diff.append(_get_commit_dict(filename=f.path, op='D'))
296
294
297 elif detail_level == 'full':
295 elif detail_level == 'full':
298 from rhodecode.lib.diffs import DiffProcessor
296 from rhodecode.lib.diffs import DiffProcessor
299 diff_processor = DiffProcessor(commit.diff())
297 diff_processor = DiffProcessor(commit.diff())
300 for dp in diff_processor.prepare():
298 for dp in diff_processor.prepare():
301 del dp['stats']['ops']
299 del dp['stats']['ops']
302 _stats = dp['stats']
300 _stats = dp['stats']
303 parsed_diff.append(_get_commit_dict(
301 parsed_diff.append(_get_commit_dict(
304 filename=dp['filename'], op=dp['operation'],
302 filename=dp['filename'], op=dp['operation'],
305 new_revision=dp['new_revision'],
303 new_revision=dp['new_revision'],
306 old_revision=dp['old_revision'],
304 old_revision=dp['old_revision'],
307 raw_diff=dp['raw_diff'], stats=_stats))
305 raw_diff=dp['raw_diff'], stats=_stats))
308
306
309 return parsed_diff
307 return parsed_diff
310
308
311
309
312 def get_commit_or_error(ref, repo):
310 def get_commit_or_error(ref, repo):
313 try:
311 try:
314 ref_type, _, ref_hash = ref.split(':')
312 ref_type, _, ref_hash = ref.split(':')
315 except ValueError:
313 except ValueError:
316 raise JSONRPCError(
314 raise JSONRPCError(
317 'Ref `{ref}` given in a wrong format. Please check the API'
315 'Ref `{ref}` given in a wrong format. Please check the API'
318 ' documentation for more details'.format(ref=ref))
316 ' documentation for more details'.format(ref=ref))
319 try:
317 try:
320 # TODO: dan: refactor this to use repo.scm_instance().get_commit()
318 # TODO: dan: refactor this to use repo.scm_instance().get_commit()
321 # once get_commit supports ref_types
319 # once get_commit supports ref_types
322 return get_commit_from_ref_name(repo, ref_hash)
320 return get_commit_from_ref_name(repo, ref_hash)
323 except RepositoryError:
321 except RepositoryError:
324 raise JSONRPCError('Ref `{ref}` does not exist'.format(ref=ref))
322 raise JSONRPCError('Ref `{ref}` does not exist'.format(ref=ref))
325
323
326
324
327 def resolve_ref_or_error(ref, repo):
325 def resolve_ref_or_error(ref, repo):
328 def _parse_ref(type_, name, hash_=None):
326 def _parse_ref(type_, name, hash_=None):
329 return type_, name, hash_
327 return type_, name, hash_
330
328
331 try:
329 try:
332 ref_type, ref_name, ref_hash = _parse_ref(*ref.split(':'))
330 ref_type, ref_name, ref_hash = _parse_ref(*ref.split(':'))
333 except TypeError:
331 except TypeError:
334 raise JSONRPCError(
332 raise JSONRPCError(
335 'Ref `{ref}` given in a wrong format. Please check the API'
333 'Ref `{ref}` given in a wrong format. Please check the API'
336 ' documentation for more details'.format(ref=ref))
334 ' documentation for more details'.format(ref=ref))
337
335
338 try:
336 try:
339 ref_hash = ref_hash or _get_ref_hash(repo, ref_type, ref_name)
337 ref_hash = ref_hash or _get_ref_hash(repo, ref_type, ref_name)
340 except (KeyError, ValueError):
338 except (KeyError, ValueError):
341 raise JSONRPCError(
339 raise JSONRPCError(
342 'The specified {type} `{name}` does not exist'.format(
340 'The specified {type} `{name}` does not exist'.format(
343 type=ref_type, name=ref_name))
341 type=ref_type, name=ref_name))
344
342
345 return ':'.join([ref_type, ref_name, ref_hash])
343 return ':'.join([ref_type, ref_name, ref_hash])
346
344
347
345
348 def _get_commit_dict(
346 def _get_commit_dict(
349 filename, op, new_revision=None, old_revision=None,
347 filename, op, new_revision=None, old_revision=None,
350 raw_diff=None, stats=None):
348 raw_diff=None, stats=None):
351 if stats is None:
349 if stats is None:
352 stats = {
350 stats = {
353 "added": None,
351 "added": None,
354 "binary": None,
352 "binary": None,
355 "deleted": None
353 "deleted": None
356 }
354 }
357 return {
355 return {
358 "filename": safe_unicode(filename),
356 "filename": safe_unicode(filename),
359 "op": op,
357 "op": op,
360
358
361 # extra details
359 # extra details
362 "new_revision": new_revision,
360 "new_revision": new_revision,
363 "old_revision": old_revision,
361 "old_revision": old_revision,
364
362
365 "raw_diff": raw_diff,
363 "raw_diff": raw_diff,
366 "stats": stats
364 "stats": stats
367 }
365 }
368
366
369
367
370 # TODO: mikhail: Think about moving this function to some library
368 # TODO: mikhail: Think about moving this function to some library
371 def _get_ref_hash(repo, type_, name):
369 def _get_ref_hash(repo, type_, name):
372 vcs_repo = repo.scm_instance()
370 vcs_repo = repo.scm_instance()
373 if type_ == 'branch' and vcs_repo.alias in ('hg', 'git'):
371 if type_ == 'branch' and vcs_repo.alias in ('hg', 'git'):
374 return vcs_repo.branches[name]
372 return vcs_repo.branches[name]
375 elif type_ == 'bookmark' and vcs_repo.alias == 'hg':
373 elif type_ == 'bookmark' and vcs_repo.alias == 'hg':
376 return vcs_repo.bookmarks[name]
374 return vcs_repo.bookmarks[name]
377 else:
375 else:
378 raise ValueError()
376 raise ValueError()
@@ -1,226 +1,255 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2016 RhodeCode GmbH
3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23 import time
23 import time
24
24
25 from rhodecode.api import jsonrpc_method, JSONRPCError
25 from rhodecode.api import jsonrpc_method, JSONRPCError
26 from rhodecode.api.exc import JSONRPCValidationError
26 from rhodecode.api.utils import (
27 from rhodecode.api.utils import (
27 Optional, OAttr, get_gist_or_error, get_user_or_error,
28 Optional, OAttr, get_gist_or_error, get_user_or_error,
28 has_superadmin_permission)
29 has_superadmin_permission)
29 from rhodecode.model.db import Session, or_
30 from rhodecode.model.db import Session, or_
30 from rhodecode.model.gist import Gist, GistModel
31 from rhodecode.model.gist import Gist, GistModel
31
32
32 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
33
34
34
35
35 @jsonrpc_method()
36 @jsonrpc_method()
36 def get_gist(request, apiuser, gistid, content=Optional(False)):
37 def get_gist(request, apiuser, gistid, content=Optional(False)):
37 """
38 """
38 Get the specified gist, based on the gist ID.
39 Get the specified gist, based on the gist ID.
39
40
40 :param apiuser: This is filled automatically from the |authtoken|.
41 :param apiuser: This is filled automatically from the |authtoken|.
41 :type apiuser: AuthUser
42 :type apiuser: AuthUser
42 :param gistid: Set the id of the private or public gist
43 :param gistid: Set the id of the private or public gist
43 :type gistid: str
44 :type gistid: str
44 :param content: Return the gist content. Default is false.
45 :param content: Return the gist content. Default is false.
45 :type content: Optional(bool)
46 :type content: Optional(bool)
46 """
47 """
47
48
48 gist = get_gist_or_error(gistid)
49 gist = get_gist_or_error(gistid)
49 content = Optional.extract(content)
50 content = Optional.extract(content)
50 if not has_superadmin_permission(apiuser):
51 if not has_superadmin_permission(apiuser):
51 if gist.gist_owner != apiuser.user_id:
52 if gist.gist_owner != apiuser.user_id:
52 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
53 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
53 data = gist.get_api_data()
54 data = gist.get_api_data()
54 if content:
55 if content:
55 from rhodecode.model.gist import GistModel
56 from rhodecode.model.gist import GistModel
56 rev, gist_files = GistModel().get_gist_files(gistid)
57 rev, gist_files = GistModel().get_gist_files(gistid)
57 data['content'] = dict([(x.path, x.content) for x in gist_files])
58 data['content'] = dict([(x.path, x.content) for x in gist_files])
58 return data
59 return data
59
60
60
61
61 @jsonrpc_method()
62 @jsonrpc_method()
62 def get_gists(request, apiuser, userid=Optional(OAttr('apiuser'))):
63 def get_gists(request, apiuser, userid=Optional(OAttr('apiuser'))):
63 """
64 """
64 Get all gists for given user. If userid is empty returned gists
65 Get all gists for given user. If userid is empty returned gists
65 are for user who called the api
66 are for user who called the api
66
67
67 :param apiuser: This is filled automatically from the |authtoken|.
68 :param apiuser: This is filled automatically from the |authtoken|.
68 :type apiuser: AuthUser
69 :type apiuser: AuthUser
69 :param userid: user to get gists for
70 :param userid: user to get gists for
70 :type userid: Optional(str or int)
71 :type userid: Optional(str or int)
71 """
72 """
72
73
73 if not has_superadmin_permission(apiuser):
74 if not has_superadmin_permission(apiuser):
74 # make sure normal user does not pass someone else userid,
75 # make sure normal user does not pass someone else userid,
75 # he is not allowed to do that
76 # he is not allowed to do that
76 if not isinstance(userid, Optional) and userid != apiuser.user_id:
77 if not isinstance(userid, Optional) and userid != apiuser.user_id:
77 raise JSONRPCError(
78 raise JSONRPCError(
78 'userid is not the same as your user'
79 'userid is not the same as your user'
79 )
80 )
80
81
81 if isinstance(userid, Optional):
82 if isinstance(userid, Optional):
82 user_id = apiuser.user_id
83 user_id = apiuser.user_id
83 else:
84 else:
84 user_id = get_user_or_error(userid).user_id
85 user_id = get_user_or_error(userid).user_id
85
86
86 gists = []
87 gists = []
87 _gists = Gist().query() \
88 _gists = Gist().query() \
88 .filter(or_(
89 .filter(or_(
89 Gist.gist_expires == -1, Gist.gist_expires >= time.time())) \
90 Gist.gist_expires == -1, Gist.gist_expires >= time.time())) \
90 .filter(Gist.gist_owner == user_id) \
91 .filter(Gist.gist_owner == user_id) \
91 .order_by(Gist.created_on.desc())
92 .order_by(Gist.created_on.desc())
92 for gist in _gists:
93 for gist in _gists:
93 gists.append(gist.get_api_data())
94 gists.append(gist.get_api_data())
94 return gists
95 return gists
95
96
96
97
97 @jsonrpc_method()
98 @jsonrpc_method()
98 def create_gist(
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 gist_type=Optional(Gist.GIST_PUBLIC), lifetime=Optional(-1),
102 gist_type=Optional(Gist.GIST_PUBLIC), lifetime=Optional(-1),
101 acl_level=Optional(Gist.ACL_LEVEL_PUBLIC),
103 acl_level=Optional(Gist.ACL_LEVEL_PUBLIC),
102 description=Optional('')):
104 description=Optional('')):
103 """
105 """
104 Creates a new Gist.
106 Creates a new Gist.
105
107
106 :param apiuser: This is filled automatically from the |authtoken|.
108 :param apiuser: This is filled automatically from the |authtoken|.
107 :type apiuser: AuthUser
109 :type apiuser: AuthUser
108 :param files: files to be added to the gist. The data structure has
110 :param files: files to be added to the gist. The data structure has
109 to match the following example::
111 to match the following example::
110
112
111 {'filename': {'content':'...', 'lexer': null},
113 {'filename1': {'content':'...'}, 'filename2': {'content':'...'}}
112 'filename2': {'content':'...', 'lexer': null}}
113
114
114 :type files: dict
115 :type files: dict
116 :param gistid: Set a custom id for the gist
117 :type gistid: Optional(str)
115 :param owner: Set the gist owner, defaults to api method caller
118 :param owner: Set the gist owner, defaults to api method caller
116 :type owner: Optional(str or int)
119 :type owner: Optional(str or int)
117 :param gist_type: type of gist ``public`` or ``private``
120 :param gist_type: type of gist ``public`` or ``private``
118 :type gist_type: Optional(str)
121 :type gist_type: Optional(str)
119 :param lifetime: time in minutes of gist lifetime
122 :param lifetime: time in minutes of gist lifetime
120 :type lifetime: Optional(int)
123 :type lifetime: Optional(int)
121 :param acl_level: acl level for this gist, can be
124 :param acl_level: acl level for this gist, can be
122 ``acl_public`` or ``acl_private`` If the value is set to
125 ``acl_public`` or ``acl_private`` If the value is set to
123 ``acl_private`` only logged in users are able to access this gist.
126 ``acl_private`` only logged in users are able to access this gist.
124 If not set it defaults to ``acl_public``.
127 If not set it defaults to ``acl_public``.
125 :type acl_level: Optional(str)
128 :type acl_level: Optional(str)
126 :param description: gist description
129 :param description: gist description
127 :type description: Optional(str)
130 :type description: Optional(str)
128
131
129 Example output:
132 Example output:
130
133
131 .. code-block:: bash
134 .. code-block:: bash
132
135
133 id : <id_given_in_input>
136 id : <id_given_in_input>
134 result : {
137 result : {
135 "msg": "created new gist",
138 "msg": "created new gist",
136 "gist": {}
139 "gist": {}
137 }
140 }
138 error : null
141 error : null
139
142
140 Example error output:
143 Example error output:
141
144
142 .. code-block:: bash
145 .. code-block:: bash
143
146
144 id : <id_given_in_input>
147 id : <id_given_in_input>
145 result : null
148 result : null
146 error : {
149 error : {
147 "failed to create gist"
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 if isinstance(owner, Optional):
157 if isinstance(owner, Optional):
154 owner = apiuser.user_id
158 owner = apiuser.user_id
155
159
156 owner = get_user_or_error(owner)
160 owner = get_user_or_error(owner)
157 description = Optional.extract(description)
161
158 gist_type = Optional.extract(gist_type)
159 lifetime = Optional.extract(lifetime)
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 owner=owner,
190 owner=owner,
164 gist_mapping=files,
191 gist_id=schema_data['gistid'],
165 gist_type=gist_type,
192 description=schema_data['description'],
166 lifetime=lifetime,
193 gist_mapping=schema_data['nodes'],
167 gist_acl_level=acl_level)
194 gist_type=schema_data['gist_type'],
195 lifetime=schema_data['lifetime'],
196 gist_acl_level=schema_data['gist_acl_level'])
168 Session().commit()
197 Session().commit()
169 return {
198 return {
170 'msg': 'created new gist',
199 'msg': 'created new gist',
171 'gist': gist.get_api_data()
200 'gist': gist.get_api_data()
172 }
201 }
173 except Exception:
202 except Exception:
174 log.exception('Error occurred during creation of gist')
203 log.exception('Error occurred during creation of gist')
175 raise JSONRPCError('failed to create gist')
204 raise JSONRPCError('failed to create gist')
176
205
177
206
178 @jsonrpc_method()
207 @jsonrpc_method()
179 def delete_gist(request, apiuser, gistid):
208 def delete_gist(request, apiuser, gistid):
180 """
209 """
181 Deletes existing gist
210 Deletes existing gist
182
211
183 :param apiuser: filled automatically from apikey
212 :param apiuser: filled automatically from apikey
184 :type apiuser: AuthUser
213 :type apiuser: AuthUser
185 :param gistid: id of gist to delete
214 :param gistid: id of gist to delete
186 :type gistid: str
215 :type gistid: str
187
216
188 Example output:
217 Example output:
189
218
190 .. code-block:: bash
219 .. code-block:: bash
191
220
192 id : <id_given_in_input>
221 id : <id_given_in_input>
193 result : {
222 result : {
194 "deleted gist ID: <gist_id>",
223 "deleted gist ID: <gist_id>",
195 "gist": null
224 "gist": null
196 }
225 }
197 error : null
226 error : null
198
227
199 Example error output:
228 Example error output:
200
229
201 .. code-block:: bash
230 .. code-block:: bash
202
231
203 id : <id_given_in_input>
232 id : <id_given_in_input>
204 result : null
233 result : null
205 error : {
234 error : {
206 "failed to delete gist ID:<gist_id>"
235 "failed to delete gist ID:<gist_id>"
207 }
236 }
208
237
209 """
238 """
210
239
211 gist = get_gist_or_error(gistid)
240 gist = get_gist_or_error(gistid)
212 if not has_superadmin_permission(apiuser):
241 if not has_superadmin_permission(apiuser):
213 if gist.gist_owner != apiuser.user_id:
242 if gist.gist_owner != apiuser.user_id:
214 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
243 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
215
244
216 try:
245 try:
217 GistModel().delete(gist)
246 GistModel().delete(gist)
218 Session().commit()
247 Session().commit()
219 return {
248 return {
220 'msg': 'deleted gist ID:%s' % (gist.gist_access_id,),
249 'msg': 'deleted gist ID:%s' % (gist.gist_access_id,),
221 'gist': None
250 'gist': None
222 }
251 }
223 except Exception:
252 except Exception:
224 log.exception('Error occured during gist deletion')
253 log.exception('Error occured during gist deletion')
225 raise JSONRPCError('failed to delete gist ID:%s'
254 raise JSONRPCError('failed to delete gist ID:%s'
226 % (gist.gist_access_id,)) No newline at end of file
255 % (gist.gist_access_id,))
@@ -1,1886 +1,1886 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2016 RhodeCode GmbH
3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import time
22 import time
23
23
24 import colander
24 import colander
25
25
26 from rhodecode import BACKENDS
26 from rhodecode import BACKENDS
27 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCForbidden, json
27 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCForbidden, json
28 from rhodecode.api.utils import (
28 from rhodecode.api.utils import (
29 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
29 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
30 get_user_group_or_error, get_user_or_error, has_repo_permissions,
30 get_user_group_or_error, get_user_or_error, has_repo_permissions,
31 get_perm_or_error, store_update, get_repo_group_or_error, parse_args,
31 get_perm_or_error, store_update, get_repo_group_or_error, parse_args,
32 get_origin, build_commit_data)
32 get_origin, build_commit_data)
33 from rhodecode.lib.auth import (
33 from rhodecode.lib.auth import (
34 HasPermissionAnyApi, HasRepoGroupPermissionAnyApi,
34 HasPermissionAnyApi, HasRepoGroupPermissionAnyApi,
35 HasUserGroupPermissionAnyApi)
35 HasUserGroupPermissionAnyApi)
36 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
36 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
37 from rhodecode.lib.utils import map_groups
37 from rhodecode.lib.utils import map_groups
38 from rhodecode.lib.utils2 import str2bool, time_to_datetime
38 from rhodecode.lib.utils2 import str2bool, time_to_datetime
39 from rhodecode.model.changeset_status import ChangesetStatusModel
39 from rhodecode.model.changeset_status import ChangesetStatusModel
40 from rhodecode.model.comment import ChangesetCommentsModel
40 from rhodecode.model.comment import ChangesetCommentsModel
41 from rhodecode.model.db import (
41 from rhodecode.model.db import (
42 Session, ChangesetStatus, RepositoryField, Repository)
42 Session, ChangesetStatus, RepositoryField, Repository)
43 from rhodecode.model.repo import RepoModel
43 from rhodecode.model.repo import RepoModel
44 from rhodecode.model.repo_group import RepoGroupModel
44 from rhodecode.model.repo_group import RepoGroupModel
45 from rhodecode.model.scm import ScmModel, RepoList
45 from rhodecode.model.scm import ScmModel, RepoList
46 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
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 log = logging.getLogger(__name__)
49 log = logging.getLogger(__name__)
50
50
51
51
52 @jsonrpc_method()
52 @jsonrpc_method()
53 def get_repo(request, apiuser, repoid, cache=Optional(True)):
53 def get_repo(request, apiuser, repoid, cache=Optional(True)):
54 """
54 """
55 Gets an existing repository by its name or repository_id.
55 Gets an existing repository by its name or repository_id.
56
56
57 The members section so the output returns users groups or users
57 The members section so the output returns users groups or users
58 associated with that repository.
58 associated with that repository.
59
59
60 This command can only be run using an |authtoken| with admin rights,
60 This command can only be run using an |authtoken| with admin rights,
61 or users with at least read rights to the |repo|.
61 or users with at least read rights to the |repo|.
62
62
63 :param apiuser: This is filled automatically from the |authtoken|.
63 :param apiuser: This is filled automatically from the |authtoken|.
64 :type apiuser: AuthUser
64 :type apiuser: AuthUser
65 :param repoid: The repository name or repository id.
65 :param repoid: The repository name or repository id.
66 :type repoid: str or int
66 :type repoid: str or int
67 :param cache: use the cached value for last changeset
67 :param cache: use the cached value for last changeset
68 :type: cache: Optional(bool)
68 :type: cache: Optional(bool)
69
69
70 Example output:
70 Example output:
71
71
72 .. code-block:: bash
72 .. code-block:: bash
73
73
74 {
74 {
75 "error": null,
75 "error": null,
76 "id": <repo_id>,
76 "id": <repo_id>,
77 "result": {
77 "result": {
78 "clone_uri": null,
78 "clone_uri": null,
79 "created_on": "timestamp",
79 "created_on": "timestamp",
80 "description": "repo description",
80 "description": "repo description",
81 "enable_downloads": false,
81 "enable_downloads": false,
82 "enable_locking": false,
82 "enable_locking": false,
83 "enable_statistics": false,
83 "enable_statistics": false,
84 "followers": [
84 "followers": [
85 {
85 {
86 "active": true,
86 "active": true,
87 "admin": false,
87 "admin": false,
88 "api_key": "****************************************",
88 "api_key": "****************************************",
89 "api_keys": [
89 "api_keys": [
90 "****************************************"
90 "****************************************"
91 ],
91 ],
92 "email": "user@example.com",
92 "email": "user@example.com",
93 "emails": [
93 "emails": [
94 "user@example.com"
94 "user@example.com"
95 ],
95 ],
96 "extern_name": "rhodecode",
96 "extern_name": "rhodecode",
97 "extern_type": "rhodecode",
97 "extern_type": "rhodecode",
98 "firstname": "username",
98 "firstname": "username",
99 "ip_addresses": [],
99 "ip_addresses": [],
100 "language": null,
100 "language": null,
101 "last_login": "2015-09-16T17:16:35.854",
101 "last_login": "2015-09-16T17:16:35.854",
102 "lastname": "surname",
102 "lastname": "surname",
103 "user_id": <user_id>,
103 "user_id": <user_id>,
104 "username": "name"
104 "username": "name"
105 }
105 }
106 ],
106 ],
107 "fork_of": "parent-repo",
107 "fork_of": "parent-repo",
108 "landing_rev": [
108 "landing_rev": [
109 "rev",
109 "rev",
110 "tip"
110 "tip"
111 ],
111 ],
112 "last_changeset": {
112 "last_changeset": {
113 "author": "User <user@example.com>",
113 "author": "User <user@example.com>",
114 "branch": "default",
114 "branch": "default",
115 "date": "timestamp",
115 "date": "timestamp",
116 "message": "last commit message",
116 "message": "last commit message",
117 "parents": [
117 "parents": [
118 {
118 {
119 "raw_id": "commit-id"
119 "raw_id": "commit-id"
120 }
120 }
121 ],
121 ],
122 "raw_id": "commit-id",
122 "raw_id": "commit-id",
123 "revision": <revision number>,
123 "revision": <revision number>,
124 "short_id": "short id"
124 "short_id": "short id"
125 },
125 },
126 "lock_reason": null,
126 "lock_reason": null,
127 "locked_by": null,
127 "locked_by": null,
128 "locked_date": null,
128 "locked_date": null,
129 "members": [
129 "members": [
130 {
130 {
131 "name": "super-admin-name",
131 "name": "super-admin-name",
132 "origin": "super-admin",
132 "origin": "super-admin",
133 "permission": "repository.admin",
133 "permission": "repository.admin",
134 "type": "user"
134 "type": "user"
135 },
135 },
136 {
136 {
137 "name": "owner-name",
137 "name": "owner-name",
138 "origin": "owner",
138 "origin": "owner",
139 "permission": "repository.admin",
139 "permission": "repository.admin",
140 "type": "user"
140 "type": "user"
141 },
141 },
142 {
142 {
143 "name": "user-group-name",
143 "name": "user-group-name",
144 "origin": "permission",
144 "origin": "permission",
145 "permission": "repository.write",
145 "permission": "repository.write",
146 "type": "user_group"
146 "type": "user_group"
147 }
147 }
148 ],
148 ],
149 "owner": "owner-name",
149 "owner": "owner-name",
150 "permissions": [
150 "permissions": [
151 {
151 {
152 "name": "super-admin-name",
152 "name": "super-admin-name",
153 "origin": "super-admin",
153 "origin": "super-admin",
154 "permission": "repository.admin",
154 "permission": "repository.admin",
155 "type": "user"
155 "type": "user"
156 },
156 },
157 {
157 {
158 "name": "owner-name",
158 "name": "owner-name",
159 "origin": "owner",
159 "origin": "owner",
160 "permission": "repository.admin",
160 "permission": "repository.admin",
161 "type": "user"
161 "type": "user"
162 },
162 },
163 {
163 {
164 "name": "user-group-name",
164 "name": "user-group-name",
165 "origin": "permission",
165 "origin": "permission",
166 "permission": "repository.write",
166 "permission": "repository.write",
167 "type": "user_group"
167 "type": "user_group"
168 }
168 }
169 ],
169 ],
170 "private": true,
170 "private": true,
171 "repo_id": 676,
171 "repo_id": 676,
172 "repo_name": "user-group/repo-name",
172 "repo_name": "user-group/repo-name",
173 "repo_type": "hg"
173 "repo_type": "hg"
174 }
174 }
175 }
175 }
176 """
176 """
177
177
178 repo = get_repo_or_error(repoid)
178 repo = get_repo_or_error(repoid)
179 cache = Optional.extract(cache)
179 cache = Optional.extract(cache)
180 include_secrets = False
180 include_secrets = False
181 if has_superadmin_permission(apiuser):
181 if has_superadmin_permission(apiuser):
182 include_secrets = True
182 include_secrets = True
183 else:
183 else:
184 # check if we have at least read permission for this repo !
184 # check if we have at least read permission for this repo !
185 _perms = (
185 _perms = (
186 'repository.admin', 'repository.write', 'repository.read',)
186 'repository.admin', 'repository.write', 'repository.read',)
187 has_repo_permissions(apiuser, repoid, repo, _perms)
187 has_repo_permissions(apiuser, repoid, repo, _perms)
188
188
189 permissions = []
189 permissions = []
190 for _user in repo.permissions():
190 for _user in repo.permissions():
191 user_data = {
191 user_data = {
192 'name': _user.username,
192 'name': _user.username,
193 'permission': _user.permission,
193 'permission': _user.permission,
194 'origin': get_origin(_user),
194 'origin': get_origin(_user),
195 'type': "user",
195 'type': "user",
196 }
196 }
197 permissions.append(user_data)
197 permissions.append(user_data)
198
198
199 for _user_group in repo.permission_user_groups():
199 for _user_group in repo.permission_user_groups():
200 user_group_data = {
200 user_group_data = {
201 'name': _user_group.users_group_name,
201 'name': _user_group.users_group_name,
202 'permission': _user_group.permission,
202 'permission': _user_group.permission,
203 'origin': get_origin(_user_group),
203 'origin': get_origin(_user_group),
204 'type': "user_group",
204 'type': "user_group",
205 }
205 }
206 permissions.append(user_group_data)
206 permissions.append(user_group_data)
207
207
208 following_users = [
208 following_users = [
209 user.user.get_api_data(include_secrets=include_secrets)
209 user.user.get_api_data(include_secrets=include_secrets)
210 for user in repo.followers]
210 for user in repo.followers]
211
211
212 if not cache:
212 if not cache:
213 repo.update_commit_cache()
213 repo.update_commit_cache()
214 data = repo.get_api_data(include_secrets=include_secrets)
214 data = repo.get_api_data(include_secrets=include_secrets)
215 data['members'] = permissions # TODO: this should be deprecated soon
215 data['members'] = permissions # TODO: this should be deprecated soon
216 data['permissions'] = permissions
216 data['permissions'] = permissions
217 data['followers'] = following_users
217 data['followers'] = following_users
218 return data
218 return data
219
219
220
220
221 @jsonrpc_method()
221 @jsonrpc_method()
222 def get_repos(request, apiuser):
222 def get_repos(request, apiuser):
223 """
223 """
224 Lists all existing repositories.
224 Lists all existing repositories.
225
225
226 This command can only be run using an |authtoken| with admin rights,
226 This command can only be run using an |authtoken| with admin rights,
227 or users with at least read rights to |repos|.
227 or users with at least read rights to |repos|.
228
228
229 :param apiuser: This is filled automatically from the |authtoken|.
229 :param apiuser: This is filled automatically from the |authtoken|.
230 :type apiuser: AuthUser
230 :type apiuser: AuthUser
231
231
232 Example output:
232 Example output:
233
233
234 .. code-block:: bash
234 .. code-block:: bash
235
235
236 id : <id_given_in_input>
236 id : <id_given_in_input>
237 result: [
237 result: [
238 {
238 {
239 "repo_id" : "<repo_id>",
239 "repo_id" : "<repo_id>",
240 "repo_name" : "<reponame>"
240 "repo_name" : "<reponame>"
241 "repo_type" : "<repo_type>",
241 "repo_type" : "<repo_type>",
242 "clone_uri" : "<clone_uri>",
242 "clone_uri" : "<clone_uri>",
243 "private": : "<bool>",
243 "private": : "<bool>",
244 "created_on" : "<datetimecreated>",
244 "created_on" : "<datetimecreated>",
245 "description" : "<description>",
245 "description" : "<description>",
246 "landing_rev": "<landing_rev>",
246 "landing_rev": "<landing_rev>",
247 "owner": "<repo_owner>",
247 "owner": "<repo_owner>",
248 "fork_of": "<name_of_fork_parent>",
248 "fork_of": "<name_of_fork_parent>",
249 "enable_downloads": "<bool>",
249 "enable_downloads": "<bool>",
250 "enable_locking": "<bool>",
250 "enable_locking": "<bool>",
251 "enable_statistics": "<bool>",
251 "enable_statistics": "<bool>",
252 },
252 },
253 ...
253 ...
254 ]
254 ]
255 error: null
255 error: null
256 """
256 """
257
257
258 include_secrets = has_superadmin_permission(apiuser)
258 include_secrets = has_superadmin_permission(apiuser)
259 _perms = ('repository.read', 'repository.write', 'repository.admin',)
259 _perms = ('repository.read', 'repository.write', 'repository.admin',)
260 extras = {'user': apiuser}
260 extras = {'user': apiuser}
261
261
262 repo_list = RepoList(
262 repo_list = RepoList(
263 RepoModel().get_all(), perm_set=_perms, extra_kwargs=extras)
263 RepoModel().get_all(), perm_set=_perms, extra_kwargs=extras)
264 return [repo.get_api_data(include_secrets=include_secrets)
264 return [repo.get_api_data(include_secrets=include_secrets)
265 for repo in repo_list]
265 for repo in repo_list]
266
266
267
267
268 @jsonrpc_method()
268 @jsonrpc_method()
269 def get_repo_changeset(request, apiuser, repoid, revision,
269 def get_repo_changeset(request, apiuser, repoid, revision,
270 details=Optional('basic')):
270 details=Optional('basic')):
271 """
271 """
272 Returns information about a changeset.
272 Returns information about a changeset.
273
273
274 Additionally parameters define the amount of details returned by
274 Additionally parameters define the amount of details returned by
275 this function.
275 this function.
276
276
277 This command can only be run using an |authtoken| with admin rights,
277 This command can only be run using an |authtoken| with admin rights,
278 or users with at least read rights to the |repo|.
278 or users with at least read rights to the |repo|.
279
279
280 :param apiuser: This is filled automatically from the |authtoken|.
280 :param apiuser: This is filled automatically from the |authtoken|.
281 :type apiuser: AuthUser
281 :type apiuser: AuthUser
282 :param repoid: The repository name or repository id
282 :param repoid: The repository name or repository id
283 :type repoid: str or int
283 :type repoid: str or int
284 :param revision: revision for which listing should be done
284 :param revision: revision for which listing should be done
285 :type revision: str
285 :type revision: str
286 :param details: details can be 'basic|extended|full' full gives diff
286 :param details: details can be 'basic|extended|full' full gives diff
287 info details like the diff itself, and number of changed files etc.
287 info details like the diff itself, and number of changed files etc.
288 :type details: Optional(str)
288 :type details: Optional(str)
289
289
290 """
290 """
291 repo = get_repo_or_error(repoid)
291 repo = get_repo_or_error(repoid)
292 if not has_superadmin_permission(apiuser):
292 if not has_superadmin_permission(apiuser):
293 _perms = (
293 _perms = (
294 'repository.admin', 'repository.write', 'repository.read',)
294 'repository.admin', 'repository.write', 'repository.read',)
295 has_repo_permissions(apiuser, repoid, repo, _perms)
295 has_repo_permissions(apiuser, repoid, repo, _perms)
296
296
297 changes_details = Optional.extract(details)
297 changes_details = Optional.extract(details)
298 _changes_details_types = ['basic', 'extended', 'full']
298 _changes_details_types = ['basic', 'extended', 'full']
299 if changes_details not in _changes_details_types:
299 if changes_details not in _changes_details_types:
300 raise JSONRPCError(
300 raise JSONRPCError(
301 'ret_type must be one of %s' % (
301 'ret_type must be one of %s' % (
302 ','.join(_changes_details_types)))
302 ','.join(_changes_details_types)))
303
303
304 pre_load = ['author', 'branch', 'date', 'message', 'parents',
304 pre_load = ['author', 'branch', 'date', 'message', 'parents',
305 'status', '_commit', '_file_paths']
305 'status', '_commit', '_file_paths']
306
306
307 try:
307 try:
308 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
308 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
309 except TypeError as e:
309 except TypeError as e:
310 raise JSONRPCError(e.message)
310 raise JSONRPCError(e.message)
311 _cs_json = cs.__json__()
311 _cs_json = cs.__json__()
312 _cs_json['diff'] = build_commit_data(cs, changes_details)
312 _cs_json['diff'] = build_commit_data(cs, changes_details)
313 if changes_details == 'full':
313 if changes_details == 'full':
314 _cs_json['refs'] = {
314 _cs_json['refs'] = {
315 'branches': [cs.branch],
315 'branches': [cs.branch],
316 'bookmarks': getattr(cs, 'bookmarks', []),
316 'bookmarks': getattr(cs, 'bookmarks', []),
317 'tags': cs.tags
317 'tags': cs.tags
318 }
318 }
319 return _cs_json
319 return _cs_json
320
320
321
321
322 @jsonrpc_method()
322 @jsonrpc_method()
323 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
323 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
324 details=Optional('basic')):
324 details=Optional('basic')):
325 """
325 """
326 Returns a set of commits limited by the number starting
326 Returns a set of commits limited by the number starting
327 from the `start_rev` option.
327 from the `start_rev` option.
328
328
329 Additional parameters define the amount of details returned by this
329 Additional parameters define the amount of details returned by this
330 function.
330 function.
331
331
332 This command can only be run using an |authtoken| with admin rights,
332 This command can only be run using an |authtoken| with admin rights,
333 or users with at least read rights to |repos|.
333 or users with at least read rights to |repos|.
334
334
335 :param apiuser: This is filled automatically from the |authtoken|.
335 :param apiuser: This is filled automatically from the |authtoken|.
336 :type apiuser: AuthUser
336 :type apiuser: AuthUser
337 :param repoid: The repository name or repository ID.
337 :param repoid: The repository name or repository ID.
338 :type repoid: str or int
338 :type repoid: str or int
339 :param start_rev: The starting revision from where to get changesets.
339 :param start_rev: The starting revision from where to get changesets.
340 :type start_rev: str
340 :type start_rev: str
341 :param limit: Limit the number of commits to this amount
341 :param limit: Limit the number of commits to this amount
342 :type limit: str or int
342 :type limit: str or int
343 :param details: Set the level of detail returned. Valid option are:
343 :param details: Set the level of detail returned. Valid option are:
344 ``basic``, ``extended`` and ``full``.
344 ``basic``, ``extended`` and ``full``.
345 :type details: Optional(str)
345 :type details: Optional(str)
346
346
347 .. note::
347 .. note::
348
348
349 Setting the parameter `details` to the value ``full`` is extensive
349 Setting the parameter `details` to the value ``full`` is extensive
350 and returns details like the diff itself, and the number
350 and returns details like the diff itself, and the number
351 of changed files.
351 of changed files.
352
352
353 """
353 """
354 repo = get_repo_or_error(repoid)
354 repo = get_repo_or_error(repoid)
355 if not has_superadmin_permission(apiuser):
355 if not has_superadmin_permission(apiuser):
356 _perms = (
356 _perms = (
357 'repository.admin', 'repository.write', 'repository.read',)
357 'repository.admin', 'repository.write', 'repository.read',)
358 has_repo_permissions(apiuser, repoid, repo, _perms)
358 has_repo_permissions(apiuser, repoid, repo, _perms)
359
359
360 changes_details = Optional.extract(details)
360 changes_details = Optional.extract(details)
361 _changes_details_types = ['basic', 'extended', 'full']
361 _changes_details_types = ['basic', 'extended', 'full']
362 if changes_details not in _changes_details_types:
362 if changes_details not in _changes_details_types:
363 raise JSONRPCError(
363 raise JSONRPCError(
364 'ret_type must be one of %s' % (
364 'ret_type must be one of %s' % (
365 ','.join(_changes_details_types)))
365 ','.join(_changes_details_types)))
366
366
367 limit = int(limit)
367 limit = int(limit)
368 pre_load = ['author', 'branch', 'date', 'message', 'parents',
368 pre_load = ['author', 'branch', 'date', 'message', 'parents',
369 'status', '_commit', '_file_paths']
369 'status', '_commit', '_file_paths']
370
370
371 vcs_repo = repo.scm_instance()
371 vcs_repo = repo.scm_instance()
372 # SVN needs a special case to distinguish its index and commit id
372 # SVN needs a special case to distinguish its index and commit id
373 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
373 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
374 start_rev = vcs_repo.commit_ids[0]
374 start_rev = vcs_repo.commit_ids[0]
375
375
376 try:
376 try:
377 commits = vcs_repo.get_commits(
377 commits = vcs_repo.get_commits(
378 start_id=start_rev, pre_load=pre_load)
378 start_id=start_rev, pre_load=pre_load)
379 except TypeError as e:
379 except TypeError as e:
380 raise JSONRPCError(e.message)
380 raise JSONRPCError(e.message)
381 except Exception:
381 except Exception:
382 log.exception('Fetching of commits failed')
382 log.exception('Fetching of commits failed')
383 raise JSONRPCError('Error occurred during commit fetching')
383 raise JSONRPCError('Error occurred during commit fetching')
384
384
385 ret = []
385 ret = []
386 for cnt, commit in enumerate(commits):
386 for cnt, commit in enumerate(commits):
387 if cnt >= limit != -1:
387 if cnt >= limit != -1:
388 break
388 break
389 _cs_json = commit.__json__()
389 _cs_json = commit.__json__()
390 _cs_json['diff'] = build_commit_data(commit, changes_details)
390 _cs_json['diff'] = build_commit_data(commit, changes_details)
391 if changes_details == 'full':
391 if changes_details == 'full':
392 _cs_json['refs'] = {
392 _cs_json['refs'] = {
393 'branches': [commit.branch],
393 'branches': [commit.branch],
394 'bookmarks': getattr(commit, 'bookmarks', []),
394 'bookmarks': getattr(commit, 'bookmarks', []),
395 'tags': commit.tags
395 'tags': commit.tags
396 }
396 }
397 ret.append(_cs_json)
397 ret.append(_cs_json)
398 return ret
398 return ret
399
399
400
400
401 @jsonrpc_method()
401 @jsonrpc_method()
402 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
402 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
403 ret_type=Optional('all'), details=Optional('basic'),
403 ret_type=Optional('all'), details=Optional('basic'),
404 max_file_bytes=Optional(None)):
404 max_file_bytes=Optional(None)):
405 """
405 """
406 Returns a list of nodes and children in a flat list for a given
406 Returns a list of nodes and children in a flat list for a given
407 path at given revision.
407 path at given revision.
408
408
409 It's possible to specify ret_type to show only `files` or `dirs`.
409 It's possible to specify ret_type to show only `files` or `dirs`.
410
410
411 This command can only be run using an |authtoken| with admin rights,
411 This command can only be run using an |authtoken| with admin rights,
412 or users with at least read rights to |repos|.
412 or users with at least read rights to |repos|.
413
413
414 :param apiuser: This is filled automatically from the |authtoken|.
414 :param apiuser: This is filled automatically from the |authtoken|.
415 :type apiuser: AuthUser
415 :type apiuser: AuthUser
416 :param repoid: The repository name or repository ID.
416 :param repoid: The repository name or repository ID.
417 :type repoid: str or int
417 :type repoid: str or int
418 :param revision: The revision for which listing should be done.
418 :param revision: The revision for which listing should be done.
419 :type revision: str
419 :type revision: str
420 :param root_path: The path from which to start displaying.
420 :param root_path: The path from which to start displaying.
421 :type root_path: str
421 :type root_path: str
422 :param ret_type: Set the return type. Valid options are
422 :param ret_type: Set the return type. Valid options are
423 ``all`` (default), ``files`` and ``dirs``.
423 ``all`` (default), ``files`` and ``dirs``.
424 :type ret_type: Optional(str)
424 :type ret_type: Optional(str)
425 :param details: Returns extended information about nodes, such as
425 :param details: Returns extended information about nodes, such as
426 md5, binary, and or content. The valid options are ``basic`` and
426 md5, binary, and or content. The valid options are ``basic`` and
427 ``full``.
427 ``full``.
428 :type details: Optional(str)
428 :type details: Optional(str)
429 :param max_file_bytes: Only return file content under this file size bytes
429 :param max_file_bytes: Only return file content under this file size bytes
430 :type details: Optional(int)
430 :type details: Optional(int)
431
431
432 Example output:
432 Example output:
433
433
434 .. code-block:: bash
434 .. code-block:: bash
435
435
436 id : <id_given_in_input>
436 id : <id_given_in_input>
437 result: [
437 result: [
438 {
438 {
439 "name" : "<name>"
439 "name" : "<name>"
440 "type" : "<type>",
440 "type" : "<type>",
441 "binary": "<true|false>" (only in extended mode)
441 "binary": "<true|false>" (only in extended mode)
442 "md5" : "<md5 of file content>" (only in extended mode)
442 "md5" : "<md5 of file content>" (only in extended mode)
443 },
443 },
444 ...
444 ...
445 ]
445 ]
446 error: null
446 error: null
447 """
447 """
448
448
449 repo = get_repo_or_error(repoid)
449 repo = get_repo_or_error(repoid)
450 if not has_superadmin_permission(apiuser):
450 if not has_superadmin_permission(apiuser):
451 _perms = (
451 _perms = (
452 'repository.admin', 'repository.write', 'repository.read',)
452 'repository.admin', 'repository.write', 'repository.read',)
453 has_repo_permissions(apiuser, repoid, repo, _perms)
453 has_repo_permissions(apiuser, repoid, repo, _perms)
454
454
455 ret_type = Optional.extract(ret_type)
455 ret_type = Optional.extract(ret_type)
456 details = Optional.extract(details)
456 details = Optional.extract(details)
457 _extended_types = ['basic', 'full']
457 _extended_types = ['basic', 'full']
458 if details not in _extended_types:
458 if details not in _extended_types:
459 raise JSONRPCError(
459 raise JSONRPCError(
460 'ret_type must be one of %s' % (','.join(_extended_types)))
460 'ret_type must be one of %s' % (','.join(_extended_types)))
461 extended_info = False
461 extended_info = False
462 content = False
462 content = False
463 if details == 'basic':
463 if details == 'basic':
464 extended_info = True
464 extended_info = True
465
465
466 if details == 'full':
466 if details == 'full':
467 extended_info = content = True
467 extended_info = content = True
468
468
469 _map = {}
469 _map = {}
470 try:
470 try:
471 # check if repo is not empty by any chance, skip quicker if it is.
471 # check if repo is not empty by any chance, skip quicker if it is.
472 _scm = repo.scm_instance()
472 _scm = repo.scm_instance()
473 if _scm.is_empty():
473 if _scm.is_empty():
474 return []
474 return []
475
475
476 _d, _f = ScmModel().get_nodes(
476 _d, _f = ScmModel().get_nodes(
477 repo, revision, root_path, flat=False,
477 repo, revision, root_path, flat=False,
478 extended_info=extended_info, content=content,
478 extended_info=extended_info, content=content,
479 max_file_bytes=max_file_bytes)
479 max_file_bytes=max_file_bytes)
480 _map = {
480 _map = {
481 'all': _d + _f,
481 'all': _d + _f,
482 'files': _f,
482 'files': _f,
483 'dirs': _d,
483 'dirs': _d,
484 }
484 }
485 return _map[ret_type]
485 return _map[ret_type]
486 except KeyError:
486 except KeyError:
487 raise JSONRPCError(
487 raise JSONRPCError(
488 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
488 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
489 except Exception:
489 except Exception:
490 log.exception("Exception occurred while trying to get repo nodes")
490 log.exception("Exception occurred while trying to get repo nodes")
491 raise JSONRPCError(
491 raise JSONRPCError(
492 'failed to get repo: `%s` nodes' % repo.repo_name
492 'failed to get repo: `%s` nodes' % repo.repo_name
493 )
493 )
494
494
495
495
496 @jsonrpc_method()
496 @jsonrpc_method()
497 def get_repo_refs(request, apiuser, repoid):
497 def get_repo_refs(request, apiuser, repoid):
498 """
498 """
499 Returns a dictionary of current references. It returns
499 Returns a dictionary of current references. It returns
500 bookmarks, branches, closed_branches, and tags for given repository
500 bookmarks, branches, closed_branches, and tags for given repository
501
501
502 It's possible to specify ret_type to show only `files` or `dirs`.
502 It's possible to specify ret_type to show only `files` or `dirs`.
503
503
504 This command can only be run using an |authtoken| with admin rights,
504 This command can only be run using an |authtoken| with admin rights,
505 or users with at least read rights to |repos|.
505 or users with at least read rights to |repos|.
506
506
507 :param apiuser: This is filled automatically from the |authtoken|.
507 :param apiuser: This is filled automatically from the |authtoken|.
508 :type apiuser: AuthUser
508 :type apiuser: AuthUser
509 :param repoid: The repository name or repository ID.
509 :param repoid: The repository name or repository ID.
510 :type repoid: str or int
510 :type repoid: str or int
511
511
512 Example output:
512 Example output:
513
513
514 .. code-block:: bash
514 .. code-block:: bash
515
515
516 id : <id_given_in_input>
516 id : <id_given_in_input>
517 result: [
517 result: [
518 TODO...
518 TODO...
519 ]
519 ]
520 error: null
520 error: null
521 """
521 """
522
522
523 repo = get_repo_or_error(repoid)
523 repo = get_repo_or_error(repoid)
524 if not has_superadmin_permission(apiuser):
524 if not has_superadmin_permission(apiuser):
525 _perms = ('repository.admin', 'repository.write', 'repository.read',)
525 _perms = ('repository.admin', 'repository.write', 'repository.read',)
526 has_repo_permissions(apiuser, repoid, repo, _perms)
526 has_repo_permissions(apiuser, repoid, repo, _perms)
527
527
528 try:
528 try:
529 # check if repo is not empty by any chance, skip quicker if it is.
529 # check if repo is not empty by any chance, skip quicker if it is.
530 vcs_instance = repo.scm_instance()
530 vcs_instance = repo.scm_instance()
531 refs = vcs_instance.refs()
531 refs = vcs_instance.refs()
532 return refs
532 return refs
533 except Exception:
533 except Exception:
534 log.exception("Exception occurred while trying to get repo refs")
534 log.exception("Exception occurred while trying to get repo refs")
535 raise JSONRPCError(
535 raise JSONRPCError(
536 'failed to get repo: `%s` references' % repo.repo_name
536 'failed to get repo: `%s` references' % repo.repo_name
537 )
537 )
538
538
539
539
540 @jsonrpc_method()
540 @jsonrpc_method()
541 def create_repo(request, apiuser, repo_name, repo_type,
541 def create_repo(request, apiuser, repo_name, repo_type,
542 owner=Optional(OAttr('apiuser')), description=Optional(''),
542 owner=Optional(OAttr('apiuser')), description=Optional(''),
543 private=Optional(False), clone_uri=Optional(None),
543 private=Optional(False), clone_uri=Optional(None),
544 landing_rev=Optional('rev:tip'),
544 landing_rev=Optional('rev:tip'),
545 enable_statistics=Optional(False),
545 enable_statistics=Optional(False),
546 enable_locking=Optional(False),
546 enable_locking=Optional(False),
547 enable_downloads=Optional(False),
547 enable_downloads=Optional(False),
548 copy_permissions=Optional(False)):
548 copy_permissions=Optional(False)):
549 """
549 """
550 Creates a repository.
550 Creates a repository.
551
551
552 * If the repository name contains "/", all the required repository
552 * If the repository name contains "/", all the required repository
553 groups will be created.
553 groups will be created.
554
554
555 For example "foo/bar/baz" will create |repo| groups "foo" and "bar"
555 For example "foo/bar/baz" will create |repo| groups "foo" and "bar"
556 (with "foo" as parent). It will also create the "baz" repository
556 (with "foo" as parent). It will also create the "baz" repository
557 with "bar" as |repo| group.
557 with "bar" as |repo| group.
558
558
559 This command can only be run using an |authtoken| with at least
559 This command can only be run using an |authtoken| with at least
560 write permissions to the |repo|.
560 write permissions to the |repo|.
561
561
562 :param apiuser: This is filled automatically from the |authtoken|.
562 :param apiuser: This is filled automatically from the |authtoken|.
563 :type apiuser: AuthUser
563 :type apiuser: AuthUser
564 :param repo_name: Set the repository name.
564 :param repo_name: Set the repository name.
565 :type repo_name: str
565 :type repo_name: str
566 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
566 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
567 :type repo_type: str
567 :type repo_type: str
568 :param owner: user_id or username
568 :param owner: user_id or username
569 :type owner: Optional(str)
569 :type owner: Optional(str)
570 :param description: Set the repository description.
570 :param description: Set the repository description.
571 :type description: Optional(str)
571 :type description: Optional(str)
572 :param private:
572 :param private:
573 :type private: bool
573 :type private: bool
574 :param clone_uri:
574 :param clone_uri:
575 :type clone_uri: str
575 :type clone_uri: str
576 :param landing_rev: <rev_type>:<rev>
576 :param landing_rev: <rev_type>:<rev>
577 :type landing_rev: str
577 :type landing_rev: str
578 :param enable_locking:
578 :param enable_locking:
579 :type enable_locking: bool
579 :type enable_locking: bool
580 :param enable_downloads:
580 :param enable_downloads:
581 :type enable_downloads: bool
581 :type enable_downloads: bool
582 :param enable_statistics:
582 :param enable_statistics:
583 :type enable_statistics: bool
583 :type enable_statistics: bool
584 :param copy_permissions: Copy permission from group in which the
584 :param copy_permissions: Copy permission from group in which the
585 repository is being created.
585 repository is being created.
586 :type copy_permissions: bool
586 :type copy_permissions: bool
587
587
588
588
589 Example output:
589 Example output:
590
590
591 .. code-block:: bash
591 .. code-block:: bash
592
592
593 id : <id_given_in_input>
593 id : <id_given_in_input>
594 result: {
594 result: {
595 "msg": "Created new repository `<reponame>`",
595 "msg": "Created new repository `<reponame>`",
596 "success": true,
596 "success": true,
597 "task": "<celery task id or None if done sync>"
597 "task": "<celery task id or None if done sync>"
598 }
598 }
599 error: null
599 error: null
600
600
601
601
602 Example error output:
602 Example error output:
603
603
604 .. code-block:: bash
604 .. code-block:: bash
605
605
606 id : <id_given_in_input>
606 id : <id_given_in_input>
607 result : null
607 result : null
608 error : {
608 error : {
609 'failed to create repository `<repo_name>`
609 'failed to create repository `<repo_name>`
610 }
610 }
611
611
612 """
612 """
613 schema = RepoSchema()
613 schema = repo_schema.RepoSchema()
614 try:
614 try:
615 data = schema.deserialize({
615 data = schema.deserialize({
616 'repo_name': repo_name
616 'repo_name': repo_name
617 })
617 })
618 except colander.Invalid as e:
618 except colander.Invalid as e:
619 raise JSONRPCError("Validation failed: %s" % (e.asdict(),))
619 raise JSONRPCError("Validation failed: %s" % (e.asdict(),))
620 repo_name = data['repo_name']
620 repo_name = data['repo_name']
621
621
622 (repo_name_cleaned,
622 (repo_name_cleaned,
623 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(
623 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(
624 repo_name)
624 repo_name)
625
625
626 if not HasPermissionAnyApi(
626 if not HasPermissionAnyApi(
627 'hg.admin', 'hg.create.repository')(user=apiuser):
627 'hg.admin', 'hg.create.repository')(user=apiuser):
628 # check if we have admin permission for this repo group if given !
628 # check if we have admin permission for this repo group if given !
629
629
630 if parent_group_name:
630 if parent_group_name:
631 repogroupid = parent_group_name
631 repogroupid = parent_group_name
632 repo_group = get_repo_group_or_error(parent_group_name)
632 repo_group = get_repo_group_or_error(parent_group_name)
633
633
634 _perms = ('group.admin',)
634 _perms = ('group.admin',)
635 if not HasRepoGroupPermissionAnyApi(*_perms)(
635 if not HasRepoGroupPermissionAnyApi(*_perms)(
636 user=apiuser, group_name=repo_group.group_name):
636 user=apiuser, group_name=repo_group.group_name):
637 raise JSONRPCError(
637 raise JSONRPCError(
638 'repository group `%s` does not exist' % (
638 'repository group `%s` does not exist' % (
639 repogroupid,))
639 repogroupid,))
640 else:
640 else:
641 raise JSONRPCForbidden()
641 raise JSONRPCForbidden()
642
642
643 if not has_superadmin_permission(apiuser):
643 if not has_superadmin_permission(apiuser):
644 if not isinstance(owner, Optional):
644 if not isinstance(owner, Optional):
645 # forbid setting owner for non-admins
645 # forbid setting owner for non-admins
646 raise JSONRPCError(
646 raise JSONRPCError(
647 'Only RhodeCode admin can specify `owner` param')
647 'Only RhodeCode admin can specify `owner` param')
648
648
649 if isinstance(owner, Optional):
649 if isinstance(owner, Optional):
650 owner = apiuser.user_id
650 owner = apiuser.user_id
651
651
652 owner = get_user_or_error(owner)
652 owner = get_user_or_error(owner)
653
653
654 if RepoModel().get_by_repo_name(repo_name):
654 if RepoModel().get_by_repo_name(repo_name):
655 raise JSONRPCError("repo `%s` already exist" % repo_name)
655 raise JSONRPCError("repo `%s` already exist" % repo_name)
656
656
657 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
657 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
658 if isinstance(private, Optional):
658 if isinstance(private, Optional):
659 private = defs.get('repo_private') or Optional.extract(private)
659 private = defs.get('repo_private') or Optional.extract(private)
660 if isinstance(repo_type, Optional):
660 if isinstance(repo_type, Optional):
661 repo_type = defs.get('repo_type')
661 repo_type = defs.get('repo_type')
662 if isinstance(enable_statistics, Optional):
662 if isinstance(enable_statistics, Optional):
663 enable_statistics = defs.get('repo_enable_statistics')
663 enable_statistics = defs.get('repo_enable_statistics')
664 if isinstance(enable_locking, Optional):
664 if isinstance(enable_locking, Optional):
665 enable_locking = defs.get('repo_enable_locking')
665 enable_locking = defs.get('repo_enable_locking')
666 if isinstance(enable_downloads, Optional):
666 if isinstance(enable_downloads, Optional):
667 enable_downloads = defs.get('repo_enable_downloads')
667 enable_downloads = defs.get('repo_enable_downloads')
668
668
669 clone_uri = Optional.extract(clone_uri)
669 clone_uri = Optional.extract(clone_uri)
670 description = Optional.extract(description)
670 description = Optional.extract(description)
671 landing_rev = Optional.extract(landing_rev)
671 landing_rev = Optional.extract(landing_rev)
672 copy_permissions = Optional.extract(copy_permissions)
672 copy_permissions = Optional.extract(copy_permissions)
673
673
674 try:
674 try:
675 # create structure of groups and return the last group
675 # create structure of groups and return the last group
676 repo_group = map_groups(repo_name)
676 repo_group = map_groups(repo_name)
677 data = {
677 data = {
678 'repo_name': repo_name_cleaned,
678 'repo_name': repo_name_cleaned,
679 'repo_name_full': repo_name,
679 'repo_name_full': repo_name,
680 'repo_type': repo_type,
680 'repo_type': repo_type,
681 'repo_description': description,
681 'repo_description': description,
682 'owner': owner,
682 'owner': owner,
683 'repo_private': private,
683 'repo_private': private,
684 'clone_uri': clone_uri,
684 'clone_uri': clone_uri,
685 'repo_group': repo_group.group_id if repo_group else None,
685 'repo_group': repo_group.group_id if repo_group else None,
686 'repo_landing_rev': landing_rev,
686 'repo_landing_rev': landing_rev,
687 'enable_statistics': enable_statistics,
687 'enable_statistics': enable_statistics,
688 'enable_locking': enable_locking,
688 'enable_locking': enable_locking,
689 'enable_downloads': enable_downloads,
689 'enable_downloads': enable_downloads,
690 'repo_copy_permissions': copy_permissions,
690 'repo_copy_permissions': copy_permissions,
691 }
691 }
692
692
693 if repo_type not in BACKENDS.keys():
693 if repo_type not in BACKENDS.keys():
694 raise Exception("Invalid backend type %s" % repo_type)
694 raise Exception("Invalid backend type %s" % repo_type)
695 task = RepoModel().create(form_data=data, cur_user=owner)
695 task = RepoModel().create(form_data=data, cur_user=owner)
696 from celery.result import BaseAsyncResult
696 from celery.result import BaseAsyncResult
697 task_id = None
697 task_id = None
698 if isinstance(task, BaseAsyncResult):
698 if isinstance(task, BaseAsyncResult):
699 task_id = task.task_id
699 task_id = task.task_id
700 # no commit, it's done in RepoModel, or async via celery
700 # no commit, it's done in RepoModel, or async via celery
701 return {
701 return {
702 'msg': "Created new repository `%s`" % (repo_name,),
702 'msg': "Created new repository `%s`" % (repo_name,),
703 'success': True, # cannot return the repo data here since fork
703 'success': True, # cannot return the repo data here since fork
704 # cann be done async
704 # cann be done async
705 'task': task_id
705 'task': task_id
706 }
706 }
707 except Exception:
707 except Exception:
708 log.exception(
708 log.exception(
709 u"Exception while trying to create the repository %s",
709 u"Exception while trying to create the repository %s",
710 repo_name)
710 repo_name)
711 raise JSONRPCError(
711 raise JSONRPCError(
712 'failed to create repository `%s`' % (repo_name,))
712 'failed to create repository `%s`' % (repo_name,))
713
713
714
714
715 @jsonrpc_method()
715 @jsonrpc_method()
716 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
716 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
717 description=Optional('')):
717 description=Optional('')):
718 """
718 """
719 Adds an extra field to a repository.
719 Adds an extra field to a repository.
720
720
721 This command can only be run using an |authtoken| with at least
721 This command can only be run using an |authtoken| with at least
722 write permissions to the |repo|.
722 write permissions to the |repo|.
723
723
724 :param apiuser: This is filled automatically from the |authtoken|.
724 :param apiuser: This is filled automatically from the |authtoken|.
725 :type apiuser: AuthUser
725 :type apiuser: AuthUser
726 :param repoid: Set the repository name or repository id.
726 :param repoid: Set the repository name or repository id.
727 :type repoid: str or int
727 :type repoid: str or int
728 :param key: Create a unique field key for this repository.
728 :param key: Create a unique field key for this repository.
729 :type key: str
729 :type key: str
730 :param label:
730 :param label:
731 :type label: Optional(str)
731 :type label: Optional(str)
732 :param description:
732 :param description:
733 :type description: Optional(str)
733 :type description: Optional(str)
734 """
734 """
735 repo = get_repo_or_error(repoid)
735 repo = get_repo_or_error(repoid)
736 if not has_superadmin_permission(apiuser):
736 if not has_superadmin_permission(apiuser):
737 _perms = ('repository.admin',)
737 _perms = ('repository.admin',)
738 has_repo_permissions(apiuser, repoid, repo, _perms)
738 has_repo_permissions(apiuser, repoid, repo, _perms)
739
739
740 label = Optional.extract(label) or key
740 label = Optional.extract(label) or key
741 description = Optional.extract(description)
741 description = Optional.extract(description)
742
742
743 field = RepositoryField.get_by_key_name(key, repo)
743 field = RepositoryField.get_by_key_name(key, repo)
744 if field:
744 if field:
745 raise JSONRPCError('Field with key '
745 raise JSONRPCError('Field with key '
746 '`%s` exists for repo `%s`' % (key, repoid))
746 '`%s` exists for repo `%s`' % (key, repoid))
747
747
748 try:
748 try:
749 RepoModel().add_repo_field(repo, key, field_label=label,
749 RepoModel().add_repo_field(repo, key, field_label=label,
750 field_desc=description)
750 field_desc=description)
751 Session().commit()
751 Session().commit()
752 return {
752 return {
753 'msg': "Added new repository field `%s`" % (key,),
753 'msg': "Added new repository field `%s`" % (key,),
754 'success': True,
754 'success': True,
755 }
755 }
756 except Exception:
756 except Exception:
757 log.exception("Exception occurred while trying to add field to repo")
757 log.exception("Exception occurred while trying to add field to repo")
758 raise JSONRPCError(
758 raise JSONRPCError(
759 'failed to create new field for repository `%s`' % (repoid,))
759 'failed to create new field for repository `%s`' % (repoid,))
760
760
761
761
762 @jsonrpc_method()
762 @jsonrpc_method()
763 def remove_field_from_repo(request, apiuser, repoid, key):
763 def remove_field_from_repo(request, apiuser, repoid, key):
764 """
764 """
765 Removes an extra field from a repository.
765 Removes an extra field from a repository.
766
766
767 This command can only be run using an |authtoken| with at least
767 This command can only be run using an |authtoken| with at least
768 write permissions to the |repo|.
768 write permissions to the |repo|.
769
769
770 :param apiuser: This is filled automatically from the |authtoken|.
770 :param apiuser: This is filled automatically from the |authtoken|.
771 :type apiuser: AuthUser
771 :type apiuser: AuthUser
772 :param repoid: Set the repository name or repository ID.
772 :param repoid: Set the repository name or repository ID.
773 :type repoid: str or int
773 :type repoid: str or int
774 :param key: Set the unique field key for this repository.
774 :param key: Set the unique field key for this repository.
775 :type key: str
775 :type key: str
776 """
776 """
777
777
778 repo = get_repo_or_error(repoid)
778 repo = get_repo_or_error(repoid)
779 if not has_superadmin_permission(apiuser):
779 if not has_superadmin_permission(apiuser):
780 _perms = ('repository.admin',)
780 _perms = ('repository.admin',)
781 has_repo_permissions(apiuser, repoid, repo, _perms)
781 has_repo_permissions(apiuser, repoid, repo, _perms)
782
782
783 field = RepositoryField.get_by_key_name(key, repo)
783 field = RepositoryField.get_by_key_name(key, repo)
784 if not field:
784 if not field:
785 raise JSONRPCError('Field with key `%s` does not '
785 raise JSONRPCError('Field with key `%s` does not '
786 'exists for repo `%s`' % (key, repoid))
786 'exists for repo `%s`' % (key, repoid))
787
787
788 try:
788 try:
789 RepoModel().delete_repo_field(repo, field_key=key)
789 RepoModel().delete_repo_field(repo, field_key=key)
790 Session().commit()
790 Session().commit()
791 return {
791 return {
792 'msg': "Deleted repository field `%s`" % (key,),
792 'msg': "Deleted repository field `%s`" % (key,),
793 'success': True,
793 'success': True,
794 }
794 }
795 except Exception:
795 except Exception:
796 log.exception(
796 log.exception(
797 "Exception occurred while trying to delete field from repo")
797 "Exception occurred while trying to delete field from repo")
798 raise JSONRPCError(
798 raise JSONRPCError(
799 'failed to delete field for repository `%s`' % (repoid,))
799 'failed to delete field for repository `%s`' % (repoid,))
800
800
801
801
802 @jsonrpc_method()
802 @jsonrpc_method()
803 def update_repo(request, apiuser, repoid, name=Optional(None),
803 def update_repo(request, apiuser, repoid, name=Optional(None),
804 owner=Optional(OAttr('apiuser')),
804 owner=Optional(OAttr('apiuser')),
805 group=Optional(None),
805 group=Optional(None),
806 fork_of=Optional(None),
806 fork_of=Optional(None),
807 description=Optional(''), private=Optional(False),
807 description=Optional(''), private=Optional(False),
808 clone_uri=Optional(None), landing_rev=Optional('rev:tip'),
808 clone_uri=Optional(None), landing_rev=Optional('rev:tip'),
809 enable_statistics=Optional(False),
809 enable_statistics=Optional(False),
810 enable_locking=Optional(False),
810 enable_locking=Optional(False),
811 enable_downloads=Optional(False),
811 enable_downloads=Optional(False),
812 fields=Optional('')):
812 fields=Optional('')):
813 """
813 """
814 Updates a repository with the given information.
814 Updates a repository with the given information.
815
815
816 This command can only be run using an |authtoken| with at least
816 This command can only be run using an |authtoken| with at least
817 write permissions to the |repo|.
817 write permissions to the |repo|.
818
818
819 :param apiuser: This is filled automatically from the |authtoken|.
819 :param apiuser: This is filled automatically from the |authtoken|.
820 :type apiuser: AuthUser
820 :type apiuser: AuthUser
821 :param repoid: repository name or repository ID.
821 :param repoid: repository name or repository ID.
822 :type repoid: str or int
822 :type repoid: str or int
823 :param name: Update the |repo| name.
823 :param name: Update the |repo| name.
824 :type name: str
824 :type name: str
825 :param owner: Set the |repo| owner.
825 :param owner: Set the |repo| owner.
826 :type owner: str
826 :type owner: str
827 :param group: Set the |repo| group the |repo| belongs to.
827 :param group: Set the |repo| group the |repo| belongs to.
828 :type group: str
828 :type group: str
829 :param fork_of: Set the master |repo| name.
829 :param fork_of: Set the master |repo| name.
830 :type fork_of: str
830 :type fork_of: str
831 :param description: Update the |repo| description.
831 :param description: Update the |repo| description.
832 :type description: str
832 :type description: str
833 :param private: Set the |repo| as private. (True | False)
833 :param private: Set the |repo| as private. (True | False)
834 :type private: bool
834 :type private: bool
835 :param clone_uri: Update the |repo| clone URI.
835 :param clone_uri: Update the |repo| clone URI.
836 :type clone_uri: str
836 :type clone_uri: str
837 :param landing_rev: Set the |repo| landing revision. Default is
837 :param landing_rev: Set the |repo| landing revision. Default is
838 ``tip``.
838 ``tip``.
839 :type landing_rev: str
839 :type landing_rev: str
840 :param enable_statistics: Enable statistics on the |repo|,
840 :param enable_statistics: Enable statistics on the |repo|,
841 (True | False).
841 (True | False).
842 :type enable_statistics: bool
842 :type enable_statistics: bool
843 :param enable_locking: Enable |repo| locking.
843 :param enable_locking: Enable |repo| locking.
844 :type enable_locking: bool
844 :type enable_locking: bool
845 :param enable_downloads: Enable downloads from the |repo|,
845 :param enable_downloads: Enable downloads from the |repo|,
846 (True | False).
846 (True | False).
847 :type enable_downloads: bool
847 :type enable_downloads: bool
848 :param fields: Add extra fields to the |repo|. Use the following
848 :param fields: Add extra fields to the |repo|. Use the following
849 example format: ``field_key=field_val,field_key2=fieldval2``.
849 example format: ``field_key=field_val,field_key2=fieldval2``.
850 Escape ', ' with \,
850 Escape ', ' with \,
851 :type fields: str
851 :type fields: str
852 """
852 """
853 repo = get_repo_or_error(repoid)
853 repo = get_repo_or_error(repoid)
854 include_secrets = False
854 include_secrets = False
855 if has_superadmin_permission(apiuser):
855 if has_superadmin_permission(apiuser):
856 include_secrets = True
856 include_secrets = True
857 else:
857 else:
858 _perms = ('repository.admin',)
858 _perms = ('repository.admin',)
859 has_repo_permissions(apiuser, repoid, repo, _perms)
859 has_repo_permissions(apiuser, repoid, repo, _perms)
860
860
861 updates = {
861 updates = {
862 # update function requires this.
862 # update function requires this.
863 'repo_name': repo.just_name
863 'repo_name': repo.just_name
864 }
864 }
865 repo_group = group
865 repo_group = group
866 if not isinstance(repo_group, Optional):
866 if not isinstance(repo_group, Optional):
867 repo_group = get_repo_group_or_error(repo_group)
867 repo_group = get_repo_group_or_error(repo_group)
868 repo_group = repo_group.group_id
868 repo_group = repo_group.group_id
869
869
870 repo_fork_of = fork_of
870 repo_fork_of = fork_of
871 if not isinstance(repo_fork_of, Optional):
871 if not isinstance(repo_fork_of, Optional):
872 repo_fork_of = get_repo_or_error(repo_fork_of)
872 repo_fork_of = get_repo_or_error(repo_fork_of)
873 repo_fork_of = repo_fork_of.repo_id
873 repo_fork_of = repo_fork_of.repo_id
874
874
875 try:
875 try:
876 store_update(updates, name, 'repo_name')
876 store_update(updates, name, 'repo_name')
877 store_update(updates, repo_group, 'repo_group')
877 store_update(updates, repo_group, 'repo_group')
878 store_update(updates, repo_fork_of, 'fork_id')
878 store_update(updates, repo_fork_of, 'fork_id')
879 store_update(updates, owner, 'user')
879 store_update(updates, owner, 'user')
880 store_update(updates, description, 'repo_description')
880 store_update(updates, description, 'repo_description')
881 store_update(updates, private, 'repo_private')
881 store_update(updates, private, 'repo_private')
882 store_update(updates, clone_uri, 'clone_uri')
882 store_update(updates, clone_uri, 'clone_uri')
883 store_update(updates, landing_rev, 'repo_landing_rev')
883 store_update(updates, landing_rev, 'repo_landing_rev')
884 store_update(updates, enable_statistics, 'repo_enable_statistics')
884 store_update(updates, enable_statistics, 'repo_enable_statistics')
885 store_update(updates, enable_locking, 'repo_enable_locking')
885 store_update(updates, enable_locking, 'repo_enable_locking')
886 store_update(updates, enable_downloads, 'repo_enable_downloads')
886 store_update(updates, enable_downloads, 'repo_enable_downloads')
887
887
888 # extra fields
888 # extra fields
889 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
889 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
890 if fields:
890 if fields:
891 updates.update(fields)
891 updates.update(fields)
892
892
893 RepoModel().update(repo, **updates)
893 RepoModel().update(repo, **updates)
894 Session().commit()
894 Session().commit()
895 return {
895 return {
896 'msg': 'updated repo ID:%s %s' % (
896 'msg': 'updated repo ID:%s %s' % (
897 repo.repo_id, repo.repo_name),
897 repo.repo_id, repo.repo_name),
898 'repository': repo.get_api_data(
898 'repository': repo.get_api_data(
899 include_secrets=include_secrets)
899 include_secrets=include_secrets)
900 }
900 }
901 except Exception:
901 except Exception:
902 log.exception(
902 log.exception(
903 u"Exception while trying to update the repository %s",
903 u"Exception while trying to update the repository %s",
904 repoid)
904 repoid)
905 raise JSONRPCError('failed to update repo `%s`' % repoid)
905 raise JSONRPCError('failed to update repo `%s`' % repoid)
906
906
907
907
908 @jsonrpc_method()
908 @jsonrpc_method()
909 def fork_repo(request, apiuser, repoid, fork_name,
909 def fork_repo(request, apiuser, repoid, fork_name,
910 owner=Optional(OAttr('apiuser')),
910 owner=Optional(OAttr('apiuser')),
911 description=Optional(''), copy_permissions=Optional(False),
911 description=Optional(''), copy_permissions=Optional(False),
912 private=Optional(False), landing_rev=Optional('rev:tip')):
912 private=Optional(False), landing_rev=Optional('rev:tip')):
913 """
913 """
914 Creates a fork of the specified |repo|.
914 Creates a fork of the specified |repo|.
915
915
916 * If using |RCE| with Celery this will immediately return a success
916 * If using |RCE| with Celery this will immediately return a success
917 message, even though the fork will be created asynchronously.
917 message, even though the fork will be created asynchronously.
918
918
919 This command can only be run using an |authtoken| with fork
919 This command can only be run using an |authtoken| with fork
920 permissions on the |repo|.
920 permissions on the |repo|.
921
921
922 :param apiuser: This is filled automatically from the |authtoken|.
922 :param apiuser: This is filled automatically from the |authtoken|.
923 :type apiuser: AuthUser
923 :type apiuser: AuthUser
924 :param repoid: Set repository name or repository ID.
924 :param repoid: Set repository name or repository ID.
925 :type repoid: str or int
925 :type repoid: str or int
926 :param fork_name: Set the fork name.
926 :param fork_name: Set the fork name.
927 :type fork_name: str
927 :type fork_name: str
928 :param owner: Set the fork owner.
928 :param owner: Set the fork owner.
929 :type owner: str
929 :type owner: str
930 :param description: Set the fork descripton.
930 :param description: Set the fork descripton.
931 :type description: str
931 :type description: str
932 :param copy_permissions: Copy permissions from parent |repo|. The
932 :param copy_permissions: Copy permissions from parent |repo|. The
933 default is False.
933 default is False.
934 :type copy_permissions: bool
934 :type copy_permissions: bool
935 :param private: Make the fork private. The default is False.
935 :param private: Make the fork private. The default is False.
936 :type private: bool
936 :type private: bool
937 :param landing_rev: Set the landing revision. The default is tip.
937 :param landing_rev: Set the landing revision. The default is tip.
938
938
939 Example output:
939 Example output:
940
940
941 .. code-block:: bash
941 .. code-block:: bash
942
942
943 id : <id_for_response>
943 id : <id_for_response>
944 api_key : "<api_key>"
944 api_key : "<api_key>"
945 args: {
945 args: {
946 "repoid" : "<reponame or repo_id>",
946 "repoid" : "<reponame or repo_id>",
947 "fork_name": "<forkname>",
947 "fork_name": "<forkname>",
948 "owner": "<username or user_id = Optional(=apiuser)>",
948 "owner": "<username or user_id = Optional(=apiuser)>",
949 "description": "<description>",
949 "description": "<description>",
950 "copy_permissions": "<bool>",
950 "copy_permissions": "<bool>",
951 "private": "<bool>",
951 "private": "<bool>",
952 "landing_rev": "<landing_rev>"
952 "landing_rev": "<landing_rev>"
953 }
953 }
954
954
955 Example error output:
955 Example error output:
956
956
957 .. code-block:: bash
957 .. code-block:: bash
958
958
959 id : <id_given_in_input>
959 id : <id_given_in_input>
960 result: {
960 result: {
961 "msg": "Created fork of `<reponame>` as `<forkname>`",
961 "msg": "Created fork of `<reponame>` as `<forkname>`",
962 "success": true,
962 "success": true,
963 "task": "<celery task id or None if done sync>"
963 "task": "<celery task id or None if done sync>"
964 }
964 }
965 error: null
965 error: null
966
966
967 """
967 """
968 if not has_superadmin_permission(apiuser):
968 if not has_superadmin_permission(apiuser):
969 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
969 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
970 raise JSONRPCForbidden()
970 raise JSONRPCForbidden()
971
971
972 repo = get_repo_or_error(repoid)
972 repo = get_repo_or_error(repoid)
973 repo_name = repo.repo_name
973 repo_name = repo.repo_name
974
974
975 (fork_name_cleaned,
975 (fork_name_cleaned,
976 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(
976 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(
977 fork_name)
977 fork_name)
978
978
979 if not has_superadmin_permission(apiuser):
979 if not has_superadmin_permission(apiuser):
980 # check if we have at least read permission for
980 # check if we have at least read permission for
981 # this repo that we fork !
981 # this repo that we fork !
982 _perms = (
982 _perms = (
983 'repository.admin', 'repository.write', 'repository.read')
983 'repository.admin', 'repository.write', 'repository.read')
984 has_repo_permissions(apiuser, repoid, repo, _perms)
984 has_repo_permissions(apiuser, repoid, repo, _perms)
985
985
986 if not isinstance(owner, Optional):
986 if not isinstance(owner, Optional):
987 # forbid setting owner for non super admins
987 # forbid setting owner for non super admins
988 raise JSONRPCError(
988 raise JSONRPCError(
989 'Only RhodeCode admin can specify `owner` param'
989 'Only RhodeCode admin can specify `owner` param'
990 )
990 )
991 # check if we have a create.repo permission if not maybe the parent
991 # check if we have a create.repo permission if not maybe the parent
992 # group permission
992 # group permission
993 if not HasPermissionAnyApi('hg.create.repository')(user=apiuser):
993 if not HasPermissionAnyApi('hg.create.repository')(user=apiuser):
994 if parent_group_name:
994 if parent_group_name:
995 repogroupid = parent_group_name
995 repogroupid = parent_group_name
996 repo_group = get_repo_group_or_error(parent_group_name)
996 repo_group = get_repo_group_or_error(parent_group_name)
997
997
998 _perms = ('group.admin',)
998 _perms = ('group.admin',)
999 if not HasRepoGroupPermissionAnyApi(*_perms)(
999 if not HasRepoGroupPermissionAnyApi(*_perms)(
1000 user=apiuser, group_name=repo_group.group_name):
1000 user=apiuser, group_name=repo_group.group_name):
1001 raise JSONRPCError(
1001 raise JSONRPCError(
1002 'repository group `%s` does not exist' % (
1002 'repository group `%s` does not exist' % (
1003 repogroupid,))
1003 repogroupid,))
1004 else:
1004 else:
1005 raise JSONRPCForbidden()
1005 raise JSONRPCForbidden()
1006
1006
1007 _repo = RepoModel().get_by_repo_name(fork_name)
1007 _repo = RepoModel().get_by_repo_name(fork_name)
1008 if _repo:
1008 if _repo:
1009 type_ = 'fork' if _repo.fork else 'repo'
1009 type_ = 'fork' if _repo.fork else 'repo'
1010 raise JSONRPCError("%s `%s` already exist" % (type_, fork_name))
1010 raise JSONRPCError("%s `%s` already exist" % (type_, fork_name))
1011
1011
1012 if isinstance(owner, Optional):
1012 if isinstance(owner, Optional):
1013 owner = apiuser.user_id
1013 owner = apiuser.user_id
1014
1014
1015 owner = get_user_or_error(owner)
1015 owner = get_user_or_error(owner)
1016
1016
1017 try:
1017 try:
1018 # create structure of groups and return the last group
1018 # create structure of groups and return the last group
1019 repo_group = map_groups(fork_name)
1019 repo_group = map_groups(fork_name)
1020 form_data = {
1020 form_data = {
1021 'repo_name': fork_name_cleaned,
1021 'repo_name': fork_name_cleaned,
1022 'repo_name_full': fork_name,
1022 'repo_name_full': fork_name,
1023 'repo_group': repo_group.group_id if repo_group else None,
1023 'repo_group': repo_group.group_id if repo_group else None,
1024 'repo_type': repo.repo_type,
1024 'repo_type': repo.repo_type,
1025 'description': Optional.extract(description),
1025 'description': Optional.extract(description),
1026 'private': Optional.extract(private),
1026 'private': Optional.extract(private),
1027 'copy_permissions': Optional.extract(copy_permissions),
1027 'copy_permissions': Optional.extract(copy_permissions),
1028 'landing_rev': Optional.extract(landing_rev),
1028 'landing_rev': Optional.extract(landing_rev),
1029 'fork_parent_id': repo.repo_id,
1029 'fork_parent_id': repo.repo_id,
1030 }
1030 }
1031
1031
1032 task = RepoModel().create_fork(form_data, cur_user=owner)
1032 task = RepoModel().create_fork(form_data, cur_user=owner)
1033 # no commit, it's done in RepoModel, or async via celery
1033 # no commit, it's done in RepoModel, or async via celery
1034 from celery.result import BaseAsyncResult
1034 from celery.result import BaseAsyncResult
1035 task_id = None
1035 task_id = None
1036 if isinstance(task, BaseAsyncResult):
1036 if isinstance(task, BaseAsyncResult):
1037 task_id = task.task_id
1037 task_id = task.task_id
1038 return {
1038 return {
1039 'msg': 'Created fork of `%s` as `%s`' % (
1039 'msg': 'Created fork of `%s` as `%s`' % (
1040 repo.repo_name, fork_name),
1040 repo.repo_name, fork_name),
1041 'success': True, # cannot return the repo data here since fork
1041 'success': True, # cannot return the repo data here since fork
1042 # can be done async
1042 # can be done async
1043 'task': task_id
1043 'task': task_id
1044 }
1044 }
1045 except Exception:
1045 except Exception:
1046 log.exception("Exception occurred while trying to fork a repo")
1046 log.exception("Exception occurred while trying to fork a repo")
1047 raise JSONRPCError(
1047 raise JSONRPCError(
1048 'failed to fork repository `%s` as `%s`' % (
1048 'failed to fork repository `%s` as `%s`' % (
1049 repo_name, fork_name))
1049 repo_name, fork_name))
1050
1050
1051
1051
1052 @jsonrpc_method()
1052 @jsonrpc_method()
1053 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1053 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1054 """
1054 """
1055 Deletes a repository.
1055 Deletes a repository.
1056
1056
1057 * When the `forks` parameter is set it's possible to detach or delete
1057 * When the `forks` parameter is set it's possible to detach or delete
1058 forks of deleted repository.
1058 forks of deleted repository.
1059
1059
1060 This command can only be run using an |authtoken| with admin
1060 This command can only be run using an |authtoken| with admin
1061 permissions on the |repo|.
1061 permissions on the |repo|.
1062
1062
1063 :param apiuser: This is filled automatically from the |authtoken|.
1063 :param apiuser: This is filled automatically from the |authtoken|.
1064 :type apiuser: AuthUser
1064 :type apiuser: AuthUser
1065 :param repoid: Set the repository name or repository ID.
1065 :param repoid: Set the repository name or repository ID.
1066 :type repoid: str or int
1066 :type repoid: str or int
1067 :param forks: Set to `detach` or `delete` forks from the |repo|.
1067 :param forks: Set to `detach` or `delete` forks from the |repo|.
1068 :type forks: Optional(str)
1068 :type forks: Optional(str)
1069
1069
1070 Example error output:
1070 Example error output:
1071
1071
1072 .. code-block:: bash
1072 .. code-block:: bash
1073
1073
1074 id : <id_given_in_input>
1074 id : <id_given_in_input>
1075 result: {
1075 result: {
1076 "msg": "Deleted repository `<reponame>`",
1076 "msg": "Deleted repository `<reponame>`",
1077 "success": true
1077 "success": true
1078 }
1078 }
1079 error: null
1079 error: null
1080 """
1080 """
1081
1081
1082 repo = get_repo_or_error(repoid)
1082 repo = get_repo_or_error(repoid)
1083 if not has_superadmin_permission(apiuser):
1083 if not has_superadmin_permission(apiuser):
1084 _perms = ('repository.admin',)
1084 _perms = ('repository.admin',)
1085 has_repo_permissions(apiuser, repoid, repo, _perms)
1085 has_repo_permissions(apiuser, repoid, repo, _perms)
1086
1086
1087 try:
1087 try:
1088 handle_forks = Optional.extract(forks)
1088 handle_forks = Optional.extract(forks)
1089 _forks_msg = ''
1089 _forks_msg = ''
1090 _forks = [f for f in repo.forks]
1090 _forks = [f for f in repo.forks]
1091 if handle_forks == 'detach':
1091 if handle_forks == 'detach':
1092 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1092 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1093 elif handle_forks == 'delete':
1093 elif handle_forks == 'delete':
1094 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1094 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1095 elif _forks:
1095 elif _forks:
1096 raise JSONRPCError(
1096 raise JSONRPCError(
1097 'Cannot delete `%s` it still contains attached forks' %
1097 'Cannot delete `%s` it still contains attached forks' %
1098 (repo.repo_name,)
1098 (repo.repo_name,)
1099 )
1099 )
1100
1100
1101 RepoModel().delete(repo, forks=forks)
1101 RepoModel().delete(repo, forks=forks)
1102 Session().commit()
1102 Session().commit()
1103 return {
1103 return {
1104 'msg': 'Deleted repository `%s`%s' % (
1104 'msg': 'Deleted repository `%s`%s' % (
1105 repo.repo_name, _forks_msg),
1105 repo.repo_name, _forks_msg),
1106 'success': True
1106 'success': True
1107 }
1107 }
1108 except Exception:
1108 except Exception:
1109 log.exception("Exception occurred while trying to delete repo")
1109 log.exception("Exception occurred while trying to delete repo")
1110 raise JSONRPCError(
1110 raise JSONRPCError(
1111 'failed to delete repository `%s`' % (repo.repo_name,)
1111 'failed to delete repository `%s`' % (repo.repo_name,)
1112 )
1112 )
1113
1113
1114
1114
1115 #TODO: marcink, change name ?
1115 #TODO: marcink, change name ?
1116 @jsonrpc_method()
1116 @jsonrpc_method()
1117 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1117 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1118 """
1118 """
1119 Invalidates the cache for the specified repository.
1119 Invalidates the cache for the specified repository.
1120
1120
1121 This command can only be run using an |authtoken| with admin rights to
1121 This command can only be run using an |authtoken| with admin rights to
1122 the specified repository.
1122 the specified repository.
1123
1123
1124 This command takes the following options:
1124 This command takes the following options:
1125
1125
1126 :param apiuser: This is filled automatically from |authtoken|.
1126 :param apiuser: This is filled automatically from |authtoken|.
1127 :type apiuser: AuthUser
1127 :type apiuser: AuthUser
1128 :param repoid: Sets the repository name or repository ID.
1128 :param repoid: Sets the repository name or repository ID.
1129 :type repoid: str or int
1129 :type repoid: str or int
1130 :param delete_keys: This deletes the invalidated keys instead of
1130 :param delete_keys: This deletes the invalidated keys instead of
1131 just flagging them.
1131 just flagging them.
1132 :type delete_keys: Optional(``True`` | ``False``)
1132 :type delete_keys: Optional(``True`` | ``False``)
1133
1133
1134 Example output:
1134 Example output:
1135
1135
1136 .. code-block:: bash
1136 .. code-block:: bash
1137
1137
1138 id : <id_given_in_input>
1138 id : <id_given_in_input>
1139 result : {
1139 result : {
1140 'msg': Cache for repository `<repository name>` was invalidated,
1140 'msg': Cache for repository `<repository name>` was invalidated,
1141 'repository': <repository name>
1141 'repository': <repository name>
1142 }
1142 }
1143 error : null
1143 error : null
1144
1144
1145 Example error output:
1145 Example error output:
1146
1146
1147 .. code-block:: bash
1147 .. code-block:: bash
1148
1148
1149 id : <id_given_in_input>
1149 id : <id_given_in_input>
1150 result : null
1150 result : null
1151 error : {
1151 error : {
1152 'Error occurred during cache invalidation action'
1152 'Error occurred during cache invalidation action'
1153 }
1153 }
1154
1154
1155 """
1155 """
1156
1156
1157 repo = get_repo_or_error(repoid)
1157 repo = get_repo_or_error(repoid)
1158 if not has_superadmin_permission(apiuser):
1158 if not has_superadmin_permission(apiuser):
1159 _perms = ('repository.admin', 'repository.write',)
1159 _perms = ('repository.admin', 'repository.write',)
1160 has_repo_permissions(apiuser, repoid, repo, _perms)
1160 has_repo_permissions(apiuser, repoid, repo, _perms)
1161
1161
1162 delete = Optional.extract(delete_keys)
1162 delete = Optional.extract(delete_keys)
1163 try:
1163 try:
1164 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1164 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1165 return {
1165 return {
1166 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1166 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1167 'repository': repo.repo_name
1167 'repository': repo.repo_name
1168 }
1168 }
1169 except Exception:
1169 except Exception:
1170 log.exception(
1170 log.exception(
1171 "Exception occurred while trying to invalidate repo cache")
1171 "Exception occurred while trying to invalidate repo cache")
1172 raise JSONRPCError(
1172 raise JSONRPCError(
1173 'Error occurred during cache invalidation action'
1173 'Error occurred during cache invalidation action'
1174 )
1174 )
1175
1175
1176
1176
1177 #TODO: marcink, change name ?
1177 #TODO: marcink, change name ?
1178 @jsonrpc_method()
1178 @jsonrpc_method()
1179 def lock(request, apiuser, repoid, locked=Optional(None),
1179 def lock(request, apiuser, repoid, locked=Optional(None),
1180 userid=Optional(OAttr('apiuser'))):
1180 userid=Optional(OAttr('apiuser'))):
1181 """
1181 """
1182 Sets the lock state of the specified |repo| by the given user.
1182 Sets the lock state of the specified |repo| by the given user.
1183 From more information, see :ref:`repo-locking`.
1183 From more information, see :ref:`repo-locking`.
1184
1184
1185 * If the ``userid`` option is not set, the repository is locked to the
1185 * If the ``userid`` option is not set, the repository is locked to the
1186 user who called the method.
1186 user who called the method.
1187 * If the ``locked`` parameter is not set, the current lock state of the
1187 * If the ``locked`` parameter is not set, the current lock state of the
1188 repository is displayed.
1188 repository is displayed.
1189
1189
1190 This command can only be run using an |authtoken| with admin rights to
1190 This command can only be run using an |authtoken| with admin rights to
1191 the specified repository.
1191 the specified repository.
1192
1192
1193 This command takes the following options:
1193 This command takes the following options:
1194
1194
1195 :param apiuser: This is filled automatically from the |authtoken|.
1195 :param apiuser: This is filled automatically from the |authtoken|.
1196 :type apiuser: AuthUser
1196 :type apiuser: AuthUser
1197 :param repoid: Sets the repository name or repository ID.
1197 :param repoid: Sets the repository name or repository ID.
1198 :type repoid: str or int
1198 :type repoid: str or int
1199 :param locked: Sets the lock state.
1199 :param locked: Sets the lock state.
1200 :type locked: Optional(``True`` | ``False``)
1200 :type locked: Optional(``True`` | ``False``)
1201 :param userid: Set the repository lock to this user.
1201 :param userid: Set the repository lock to this user.
1202 :type userid: Optional(str or int)
1202 :type userid: Optional(str or int)
1203
1203
1204 Example error output:
1204 Example error output:
1205
1205
1206 .. code-block:: bash
1206 .. code-block:: bash
1207
1207
1208 id : <id_given_in_input>
1208 id : <id_given_in_input>
1209 result : {
1209 result : {
1210 'repo': '<reponame>',
1210 'repo': '<reponame>',
1211 'locked': <bool: lock state>,
1211 'locked': <bool: lock state>,
1212 'locked_since': <int: lock timestamp>,
1212 'locked_since': <int: lock timestamp>,
1213 'locked_by': <username of person who made the lock>,
1213 'locked_by': <username of person who made the lock>,
1214 'lock_reason': <str: reason for locking>,
1214 'lock_reason': <str: reason for locking>,
1215 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1215 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1216 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1216 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1217 or
1217 or
1218 'msg': 'Repo `<repository name>` not locked.'
1218 'msg': 'Repo `<repository name>` not locked.'
1219 or
1219 or
1220 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1220 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1221 }
1221 }
1222 error : null
1222 error : null
1223
1223
1224 Example error output:
1224 Example error output:
1225
1225
1226 .. code-block:: bash
1226 .. code-block:: bash
1227
1227
1228 id : <id_given_in_input>
1228 id : <id_given_in_input>
1229 result : null
1229 result : null
1230 error : {
1230 error : {
1231 'Error occurred locking repository `<reponame>`
1231 'Error occurred locking repository `<reponame>`
1232 }
1232 }
1233 """
1233 """
1234
1234
1235 repo = get_repo_or_error(repoid)
1235 repo = get_repo_or_error(repoid)
1236 if not has_superadmin_permission(apiuser):
1236 if not has_superadmin_permission(apiuser):
1237 # check if we have at least write permission for this repo !
1237 # check if we have at least write permission for this repo !
1238 _perms = ('repository.admin', 'repository.write',)
1238 _perms = ('repository.admin', 'repository.write',)
1239 has_repo_permissions(apiuser, repoid, repo, _perms)
1239 has_repo_permissions(apiuser, repoid, repo, _perms)
1240
1240
1241 # make sure normal user does not pass someone else userid,
1241 # make sure normal user does not pass someone else userid,
1242 # he is not allowed to do that
1242 # he is not allowed to do that
1243 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1243 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1244 raise JSONRPCError('userid is not the same as your user')
1244 raise JSONRPCError('userid is not the same as your user')
1245
1245
1246 if isinstance(userid, Optional):
1246 if isinstance(userid, Optional):
1247 userid = apiuser.user_id
1247 userid = apiuser.user_id
1248
1248
1249 user = get_user_or_error(userid)
1249 user = get_user_or_error(userid)
1250
1250
1251 if isinstance(locked, Optional):
1251 if isinstance(locked, Optional):
1252 lockobj = repo.locked
1252 lockobj = repo.locked
1253
1253
1254 if lockobj[0] is None:
1254 if lockobj[0] is None:
1255 _d = {
1255 _d = {
1256 'repo': repo.repo_name,
1256 'repo': repo.repo_name,
1257 'locked': False,
1257 'locked': False,
1258 'locked_since': None,
1258 'locked_since': None,
1259 'locked_by': None,
1259 'locked_by': None,
1260 'lock_reason': None,
1260 'lock_reason': None,
1261 'lock_state_changed': False,
1261 'lock_state_changed': False,
1262 'msg': 'Repo `%s` not locked.' % repo.repo_name
1262 'msg': 'Repo `%s` not locked.' % repo.repo_name
1263 }
1263 }
1264 return _d
1264 return _d
1265 else:
1265 else:
1266 _user_id, _time, _reason = lockobj
1266 _user_id, _time, _reason = lockobj
1267 lock_user = get_user_or_error(userid)
1267 lock_user = get_user_or_error(userid)
1268 _d = {
1268 _d = {
1269 'repo': repo.repo_name,
1269 'repo': repo.repo_name,
1270 'locked': True,
1270 'locked': True,
1271 'locked_since': _time,
1271 'locked_since': _time,
1272 'locked_by': lock_user.username,
1272 'locked_by': lock_user.username,
1273 'lock_reason': _reason,
1273 'lock_reason': _reason,
1274 'lock_state_changed': False,
1274 'lock_state_changed': False,
1275 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1275 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1276 % (repo.repo_name, lock_user.username,
1276 % (repo.repo_name, lock_user.username,
1277 json.dumps(time_to_datetime(_time))))
1277 json.dumps(time_to_datetime(_time))))
1278 }
1278 }
1279 return _d
1279 return _d
1280
1280
1281 # force locked state through a flag
1281 # force locked state through a flag
1282 else:
1282 else:
1283 locked = str2bool(locked)
1283 locked = str2bool(locked)
1284 lock_reason = Repository.LOCK_API
1284 lock_reason = Repository.LOCK_API
1285 try:
1285 try:
1286 if locked:
1286 if locked:
1287 lock_time = time.time()
1287 lock_time = time.time()
1288 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1288 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1289 else:
1289 else:
1290 lock_time = None
1290 lock_time = None
1291 Repository.unlock(repo)
1291 Repository.unlock(repo)
1292 _d = {
1292 _d = {
1293 'repo': repo.repo_name,
1293 'repo': repo.repo_name,
1294 'locked': locked,
1294 'locked': locked,
1295 'locked_since': lock_time,
1295 'locked_since': lock_time,
1296 'locked_by': user.username,
1296 'locked_by': user.username,
1297 'lock_reason': lock_reason,
1297 'lock_reason': lock_reason,
1298 'lock_state_changed': True,
1298 'lock_state_changed': True,
1299 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1299 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1300 % (user.username, repo.repo_name, locked))
1300 % (user.username, repo.repo_name, locked))
1301 }
1301 }
1302 return _d
1302 return _d
1303 except Exception:
1303 except Exception:
1304 log.exception(
1304 log.exception(
1305 "Exception occurred while trying to lock repository")
1305 "Exception occurred while trying to lock repository")
1306 raise JSONRPCError(
1306 raise JSONRPCError(
1307 'Error occurred locking repository `%s`' % repo.repo_name
1307 'Error occurred locking repository `%s`' % repo.repo_name
1308 )
1308 )
1309
1309
1310
1310
1311 @jsonrpc_method()
1311 @jsonrpc_method()
1312 def comment_commit(
1312 def comment_commit(
1313 request, apiuser, repoid, commit_id, message,
1313 request, apiuser, repoid, commit_id, message,
1314 userid=Optional(OAttr('apiuser')), status=Optional(None)):
1314 userid=Optional(OAttr('apiuser')), status=Optional(None)):
1315 """
1315 """
1316 Set a commit comment, and optionally change the status of the commit.
1316 Set a commit comment, and optionally change the status of the commit.
1317
1317
1318 :param apiuser: This is filled automatically from the |authtoken|.
1318 :param apiuser: This is filled automatically from the |authtoken|.
1319 :type apiuser: AuthUser
1319 :type apiuser: AuthUser
1320 :param repoid: Set the repository name or repository ID.
1320 :param repoid: Set the repository name or repository ID.
1321 :type repoid: str or int
1321 :type repoid: str or int
1322 :param commit_id: Specify the commit_id for which to set a comment.
1322 :param commit_id: Specify the commit_id for which to set a comment.
1323 :type commit_id: str
1323 :type commit_id: str
1324 :param message: The comment text.
1324 :param message: The comment text.
1325 :type message: str
1325 :type message: str
1326 :param userid: Set the user name of the comment creator.
1326 :param userid: Set the user name of the comment creator.
1327 :type userid: Optional(str or int)
1327 :type userid: Optional(str or int)
1328 :param status: status, one of 'not_reviewed', 'approved', 'rejected',
1328 :param status: status, one of 'not_reviewed', 'approved', 'rejected',
1329 'under_review'
1329 'under_review'
1330 :type status: str
1330 :type status: str
1331
1331
1332 Example error output:
1332 Example error output:
1333
1333
1334 .. code-block:: json
1334 .. code-block:: json
1335
1335
1336 {
1336 {
1337 "id" : <id_given_in_input>,
1337 "id" : <id_given_in_input>,
1338 "result" : {
1338 "result" : {
1339 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1339 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1340 "status_change": null or <status>,
1340 "status_change": null or <status>,
1341 "success": true
1341 "success": true
1342 },
1342 },
1343 "error" : null
1343 "error" : null
1344 }
1344 }
1345
1345
1346 """
1346 """
1347 repo = get_repo_or_error(repoid)
1347 repo = get_repo_or_error(repoid)
1348 if not has_superadmin_permission(apiuser):
1348 if not has_superadmin_permission(apiuser):
1349 _perms = ('repository.read', 'repository.write', 'repository.admin')
1349 _perms = ('repository.read', 'repository.write', 'repository.admin')
1350 has_repo_permissions(apiuser, repoid, repo, _perms)
1350 has_repo_permissions(apiuser, repoid, repo, _perms)
1351
1351
1352 if isinstance(userid, Optional):
1352 if isinstance(userid, Optional):
1353 userid = apiuser.user_id
1353 userid = apiuser.user_id
1354
1354
1355 user = get_user_or_error(userid)
1355 user = get_user_or_error(userid)
1356 status = Optional.extract(status)
1356 status = Optional.extract(status)
1357
1357
1358 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1358 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1359 if status and status not in allowed_statuses:
1359 if status and status not in allowed_statuses:
1360 raise JSONRPCError('Bad status, must be on '
1360 raise JSONRPCError('Bad status, must be on '
1361 'of %s got %s' % (allowed_statuses, status,))
1361 'of %s got %s' % (allowed_statuses, status,))
1362
1362
1363 try:
1363 try:
1364 rc_config = SettingsModel().get_all_settings()
1364 rc_config = SettingsModel().get_all_settings()
1365 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1365 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1366
1366
1367 comm = ChangesetCommentsModel().create(
1367 comm = ChangesetCommentsModel().create(
1368 message, repo, user, revision=commit_id, status_change=status,
1368 message, repo, user, revision=commit_id, status_change=status,
1369 renderer=renderer)
1369 renderer=renderer)
1370 if status:
1370 if status:
1371 # also do a status change
1371 # also do a status change
1372 try:
1372 try:
1373 ChangesetStatusModel().set_status(
1373 ChangesetStatusModel().set_status(
1374 repo, status, user, comm, revision=commit_id,
1374 repo, status, user, comm, revision=commit_id,
1375 dont_allow_on_closed_pull_request=True
1375 dont_allow_on_closed_pull_request=True
1376 )
1376 )
1377 except StatusChangeOnClosedPullRequestError:
1377 except StatusChangeOnClosedPullRequestError:
1378 log.exception(
1378 log.exception(
1379 "Exception occurred while trying to change repo commit status")
1379 "Exception occurred while trying to change repo commit status")
1380 msg = ('Changing status on a changeset associated with '
1380 msg = ('Changing status on a changeset associated with '
1381 'a closed pull request is not allowed')
1381 'a closed pull request is not allowed')
1382 raise JSONRPCError(msg)
1382 raise JSONRPCError(msg)
1383
1383
1384 Session().commit()
1384 Session().commit()
1385 return {
1385 return {
1386 'msg': (
1386 'msg': (
1387 'Commented on commit `%s` for repository `%s`' % (
1387 'Commented on commit `%s` for repository `%s`' % (
1388 comm.revision, repo.repo_name)),
1388 comm.revision, repo.repo_name)),
1389 'status_change': status,
1389 'status_change': status,
1390 'success': True,
1390 'success': True,
1391 }
1391 }
1392 except JSONRPCError:
1392 except JSONRPCError:
1393 # catch any inside errors, and re-raise them to prevent from
1393 # catch any inside errors, and re-raise them to prevent from
1394 # below global catch to silence them
1394 # below global catch to silence them
1395 raise
1395 raise
1396 except Exception:
1396 except Exception:
1397 log.exception("Exception occurred while trying to comment on commit")
1397 log.exception("Exception occurred while trying to comment on commit")
1398 raise JSONRPCError(
1398 raise JSONRPCError(
1399 'failed to set comment on repository `%s`' % (repo.repo_name,)
1399 'failed to set comment on repository `%s`' % (repo.repo_name,)
1400 )
1400 )
1401
1401
1402
1402
1403 @jsonrpc_method()
1403 @jsonrpc_method()
1404 def grant_user_permission(request, apiuser, repoid, userid, perm):
1404 def grant_user_permission(request, apiuser, repoid, userid, perm):
1405 """
1405 """
1406 Grant permissions for the specified user on the given repository,
1406 Grant permissions for the specified user on the given repository,
1407 or update existing permissions if found.
1407 or update existing permissions if found.
1408
1408
1409 This command can only be run using an |authtoken| with admin
1409 This command can only be run using an |authtoken| with admin
1410 permissions on the |repo|.
1410 permissions on the |repo|.
1411
1411
1412 :param apiuser: This is filled automatically from the |authtoken|.
1412 :param apiuser: This is filled automatically from the |authtoken|.
1413 :type apiuser: AuthUser
1413 :type apiuser: AuthUser
1414 :param repoid: Set the repository name or repository ID.
1414 :param repoid: Set the repository name or repository ID.
1415 :type repoid: str or int
1415 :type repoid: str or int
1416 :param userid: Set the user name.
1416 :param userid: Set the user name.
1417 :type userid: str
1417 :type userid: str
1418 :param perm: Set the user permissions, using the following format
1418 :param perm: Set the user permissions, using the following format
1419 ``(repository.(none|read|write|admin))``
1419 ``(repository.(none|read|write|admin))``
1420 :type perm: str
1420 :type perm: str
1421
1421
1422 Example output:
1422 Example output:
1423
1423
1424 .. code-block:: bash
1424 .. code-block:: bash
1425
1425
1426 id : <id_given_in_input>
1426 id : <id_given_in_input>
1427 result: {
1427 result: {
1428 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1428 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1429 "success": true
1429 "success": true
1430 }
1430 }
1431 error: null
1431 error: null
1432 """
1432 """
1433
1433
1434 repo = get_repo_or_error(repoid)
1434 repo = get_repo_or_error(repoid)
1435 user = get_user_or_error(userid)
1435 user = get_user_or_error(userid)
1436 perm = get_perm_or_error(perm)
1436 perm = get_perm_or_error(perm)
1437 if not has_superadmin_permission(apiuser):
1437 if not has_superadmin_permission(apiuser):
1438 _perms = ('repository.admin',)
1438 _perms = ('repository.admin',)
1439 has_repo_permissions(apiuser, repoid, repo, _perms)
1439 has_repo_permissions(apiuser, repoid, repo, _perms)
1440
1440
1441 try:
1441 try:
1442
1442
1443 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
1443 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
1444
1444
1445 Session().commit()
1445 Session().commit()
1446 return {
1446 return {
1447 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1447 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1448 perm.permission_name, user.username, repo.repo_name
1448 perm.permission_name, user.username, repo.repo_name
1449 ),
1449 ),
1450 'success': True
1450 'success': True
1451 }
1451 }
1452 except Exception:
1452 except Exception:
1453 log.exception(
1453 log.exception(
1454 "Exception occurred while trying edit permissions for repo")
1454 "Exception occurred while trying edit permissions for repo")
1455 raise JSONRPCError(
1455 raise JSONRPCError(
1456 'failed to edit permission for user: `%s` in repo: `%s`' % (
1456 'failed to edit permission for user: `%s` in repo: `%s`' % (
1457 userid, repoid
1457 userid, repoid
1458 )
1458 )
1459 )
1459 )
1460
1460
1461
1461
1462 @jsonrpc_method()
1462 @jsonrpc_method()
1463 def revoke_user_permission(request, apiuser, repoid, userid):
1463 def revoke_user_permission(request, apiuser, repoid, userid):
1464 """
1464 """
1465 Revoke permission for a user on the specified repository.
1465 Revoke permission for a user on the specified repository.
1466
1466
1467 This command can only be run using an |authtoken| with admin
1467 This command can only be run using an |authtoken| with admin
1468 permissions on the |repo|.
1468 permissions on the |repo|.
1469
1469
1470 :param apiuser: This is filled automatically from the |authtoken|.
1470 :param apiuser: This is filled automatically from the |authtoken|.
1471 :type apiuser: AuthUser
1471 :type apiuser: AuthUser
1472 :param repoid: Set the repository name or repository ID.
1472 :param repoid: Set the repository name or repository ID.
1473 :type repoid: str or int
1473 :type repoid: str or int
1474 :param userid: Set the user name of revoked user.
1474 :param userid: Set the user name of revoked user.
1475 :type userid: str or int
1475 :type userid: str or int
1476
1476
1477 Example error output:
1477 Example error output:
1478
1478
1479 .. code-block:: bash
1479 .. code-block:: bash
1480
1480
1481 id : <id_given_in_input>
1481 id : <id_given_in_input>
1482 result: {
1482 result: {
1483 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1483 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1484 "success": true
1484 "success": true
1485 }
1485 }
1486 error: null
1486 error: null
1487 """
1487 """
1488
1488
1489 repo = get_repo_or_error(repoid)
1489 repo = get_repo_or_error(repoid)
1490 user = get_user_or_error(userid)
1490 user = get_user_or_error(userid)
1491 if not has_superadmin_permission(apiuser):
1491 if not has_superadmin_permission(apiuser):
1492 _perms = ('repository.admin',)
1492 _perms = ('repository.admin',)
1493 has_repo_permissions(apiuser, repoid, repo, _perms)
1493 has_repo_permissions(apiuser, repoid, repo, _perms)
1494
1494
1495 try:
1495 try:
1496 RepoModel().revoke_user_permission(repo=repo, user=user)
1496 RepoModel().revoke_user_permission(repo=repo, user=user)
1497 Session().commit()
1497 Session().commit()
1498 return {
1498 return {
1499 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1499 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1500 user.username, repo.repo_name
1500 user.username, repo.repo_name
1501 ),
1501 ),
1502 'success': True
1502 'success': True
1503 }
1503 }
1504 except Exception:
1504 except Exception:
1505 log.exception(
1505 log.exception(
1506 "Exception occurred while trying revoke permissions to repo")
1506 "Exception occurred while trying revoke permissions to repo")
1507 raise JSONRPCError(
1507 raise JSONRPCError(
1508 'failed to edit permission for user: `%s` in repo: `%s`' % (
1508 'failed to edit permission for user: `%s` in repo: `%s`' % (
1509 userid, repoid
1509 userid, repoid
1510 )
1510 )
1511 )
1511 )
1512
1512
1513
1513
1514 @jsonrpc_method()
1514 @jsonrpc_method()
1515 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1515 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1516 """
1516 """
1517 Grant permission for a user group on the specified repository,
1517 Grant permission for a user group on the specified repository,
1518 or update existing permissions.
1518 or update existing permissions.
1519
1519
1520 This command can only be run using an |authtoken| with admin
1520 This command can only be run using an |authtoken| with admin
1521 permissions on the |repo|.
1521 permissions on the |repo|.
1522
1522
1523 :param apiuser: This is filled automatically from the |authtoken|.
1523 :param apiuser: This is filled automatically from the |authtoken|.
1524 :type apiuser: AuthUser
1524 :type apiuser: AuthUser
1525 :param repoid: Set the repository name or repository ID.
1525 :param repoid: Set the repository name or repository ID.
1526 :type repoid: str or int
1526 :type repoid: str or int
1527 :param usergroupid: Specify the ID of the user group.
1527 :param usergroupid: Specify the ID of the user group.
1528 :type usergroupid: str or int
1528 :type usergroupid: str or int
1529 :param perm: Set the user group permissions using the following
1529 :param perm: Set the user group permissions using the following
1530 format: (repository.(none|read|write|admin))
1530 format: (repository.(none|read|write|admin))
1531 :type perm: str
1531 :type perm: str
1532
1532
1533 Example output:
1533 Example output:
1534
1534
1535 .. code-block:: bash
1535 .. code-block:: bash
1536
1536
1537 id : <id_given_in_input>
1537 id : <id_given_in_input>
1538 result : {
1538 result : {
1539 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1539 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1540 "success": true
1540 "success": true
1541
1541
1542 }
1542 }
1543 error : null
1543 error : null
1544
1544
1545 Example error output:
1545 Example error output:
1546
1546
1547 .. code-block:: bash
1547 .. code-block:: bash
1548
1548
1549 id : <id_given_in_input>
1549 id : <id_given_in_input>
1550 result : null
1550 result : null
1551 error : {
1551 error : {
1552 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1552 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1553 }
1553 }
1554
1554
1555 """
1555 """
1556
1556
1557 repo = get_repo_or_error(repoid)
1557 repo = get_repo_or_error(repoid)
1558 perm = get_perm_or_error(perm)
1558 perm = get_perm_or_error(perm)
1559 if not has_superadmin_permission(apiuser):
1559 if not has_superadmin_permission(apiuser):
1560 _perms = ('repository.admin',)
1560 _perms = ('repository.admin',)
1561 has_repo_permissions(apiuser, repoid, repo, _perms)
1561 has_repo_permissions(apiuser, repoid, repo, _perms)
1562
1562
1563 user_group = get_user_group_or_error(usergroupid)
1563 user_group = get_user_group_or_error(usergroupid)
1564 if not has_superadmin_permission(apiuser):
1564 if not has_superadmin_permission(apiuser):
1565 # check if we have at least read permission for this user group !
1565 # check if we have at least read permission for this user group !
1566 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1566 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1567 if not HasUserGroupPermissionAnyApi(*_perms)(
1567 if not HasUserGroupPermissionAnyApi(*_perms)(
1568 user=apiuser, user_group_name=user_group.users_group_name):
1568 user=apiuser, user_group_name=user_group.users_group_name):
1569 raise JSONRPCError(
1569 raise JSONRPCError(
1570 'user group `%s` does not exist' % (usergroupid,))
1570 'user group `%s` does not exist' % (usergroupid,))
1571
1571
1572 try:
1572 try:
1573 RepoModel().grant_user_group_permission(
1573 RepoModel().grant_user_group_permission(
1574 repo=repo, group_name=user_group, perm=perm)
1574 repo=repo, group_name=user_group, perm=perm)
1575
1575
1576 Session().commit()
1576 Session().commit()
1577 return {
1577 return {
1578 'msg': 'Granted perm: `%s` for user group: `%s` in '
1578 'msg': 'Granted perm: `%s` for user group: `%s` in '
1579 'repo: `%s`' % (
1579 'repo: `%s`' % (
1580 perm.permission_name, user_group.users_group_name,
1580 perm.permission_name, user_group.users_group_name,
1581 repo.repo_name
1581 repo.repo_name
1582 ),
1582 ),
1583 'success': True
1583 'success': True
1584 }
1584 }
1585 except Exception:
1585 except Exception:
1586 log.exception(
1586 log.exception(
1587 "Exception occurred while trying change permission on repo")
1587 "Exception occurred while trying change permission on repo")
1588 raise JSONRPCError(
1588 raise JSONRPCError(
1589 'failed to edit permission for user group: `%s` in '
1589 'failed to edit permission for user group: `%s` in '
1590 'repo: `%s`' % (
1590 'repo: `%s`' % (
1591 usergroupid, repo.repo_name
1591 usergroupid, repo.repo_name
1592 )
1592 )
1593 )
1593 )
1594
1594
1595
1595
1596 @jsonrpc_method()
1596 @jsonrpc_method()
1597 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1597 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1598 """
1598 """
1599 Revoke the permissions of a user group on a given repository.
1599 Revoke the permissions of a user group on a given repository.
1600
1600
1601 This command can only be run using an |authtoken| with admin
1601 This command can only be run using an |authtoken| with admin
1602 permissions on the |repo|.
1602 permissions on the |repo|.
1603
1603
1604 :param apiuser: This is filled automatically from the |authtoken|.
1604 :param apiuser: This is filled automatically from the |authtoken|.
1605 :type apiuser: AuthUser
1605 :type apiuser: AuthUser
1606 :param repoid: Set the repository name or repository ID.
1606 :param repoid: Set the repository name or repository ID.
1607 :type repoid: str or int
1607 :type repoid: str or int
1608 :param usergroupid: Specify the user group ID.
1608 :param usergroupid: Specify the user group ID.
1609 :type usergroupid: str or int
1609 :type usergroupid: str or int
1610
1610
1611 Example output:
1611 Example output:
1612
1612
1613 .. code-block:: bash
1613 .. code-block:: bash
1614
1614
1615 id : <id_given_in_input>
1615 id : <id_given_in_input>
1616 result: {
1616 result: {
1617 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1617 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1618 "success": true
1618 "success": true
1619 }
1619 }
1620 error: null
1620 error: null
1621 """
1621 """
1622
1622
1623 repo = get_repo_or_error(repoid)
1623 repo = get_repo_or_error(repoid)
1624 if not has_superadmin_permission(apiuser):
1624 if not has_superadmin_permission(apiuser):
1625 _perms = ('repository.admin',)
1625 _perms = ('repository.admin',)
1626 has_repo_permissions(apiuser, repoid, repo, _perms)
1626 has_repo_permissions(apiuser, repoid, repo, _perms)
1627
1627
1628 user_group = get_user_group_or_error(usergroupid)
1628 user_group = get_user_group_or_error(usergroupid)
1629 if not has_superadmin_permission(apiuser):
1629 if not has_superadmin_permission(apiuser):
1630 # check if we have at least read permission for this user group !
1630 # check if we have at least read permission for this user group !
1631 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1631 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1632 if not HasUserGroupPermissionAnyApi(*_perms)(
1632 if not HasUserGroupPermissionAnyApi(*_perms)(
1633 user=apiuser, user_group_name=user_group.users_group_name):
1633 user=apiuser, user_group_name=user_group.users_group_name):
1634 raise JSONRPCError(
1634 raise JSONRPCError(
1635 'user group `%s` does not exist' % (usergroupid,))
1635 'user group `%s` does not exist' % (usergroupid,))
1636
1636
1637 try:
1637 try:
1638 RepoModel().revoke_user_group_permission(
1638 RepoModel().revoke_user_group_permission(
1639 repo=repo, group_name=user_group)
1639 repo=repo, group_name=user_group)
1640
1640
1641 Session().commit()
1641 Session().commit()
1642 return {
1642 return {
1643 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1643 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1644 user_group.users_group_name, repo.repo_name
1644 user_group.users_group_name, repo.repo_name
1645 ),
1645 ),
1646 'success': True
1646 'success': True
1647 }
1647 }
1648 except Exception:
1648 except Exception:
1649 log.exception("Exception occurred while trying revoke "
1649 log.exception("Exception occurred while trying revoke "
1650 "user group permission on repo")
1650 "user group permission on repo")
1651 raise JSONRPCError(
1651 raise JSONRPCError(
1652 'failed to edit permission for user group: `%s` in '
1652 'failed to edit permission for user group: `%s` in '
1653 'repo: `%s`' % (
1653 'repo: `%s`' % (
1654 user_group.users_group_name, repo.repo_name
1654 user_group.users_group_name, repo.repo_name
1655 )
1655 )
1656 )
1656 )
1657
1657
1658
1658
1659 @jsonrpc_method()
1659 @jsonrpc_method()
1660 def pull(request, apiuser, repoid):
1660 def pull(request, apiuser, repoid):
1661 """
1661 """
1662 Triggers a pull on the given repository from a remote location. You
1662 Triggers a pull on the given repository from a remote location. You
1663 can use this to keep remote repositories up-to-date.
1663 can use this to keep remote repositories up-to-date.
1664
1664
1665 This command can only be run using an |authtoken| with admin
1665 This command can only be run using an |authtoken| with admin
1666 rights to the specified repository. For more information,
1666 rights to the specified repository. For more information,
1667 see :ref:`config-token-ref`.
1667 see :ref:`config-token-ref`.
1668
1668
1669 This command takes the following options:
1669 This command takes the following options:
1670
1670
1671 :param apiuser: This is filled automatically from the |authtoken|.
1671 :param apiuser: This is filled automatically from the |authtoken|.
1672 :type apiuser: AuthUser
1672 :type apiuser: AuthUser
1673 :param repoid: The repository name or repository ID.
1673 :param repoid: The repository name or repository ID.
1674 :type repoid: str or int
1674 :type repoid: str or int
1675
1675
1676 Example output:
1676 Example output:
1677
1677
1678 .. code-block:: bash
1678 .. code-block:: bash
1679
1679
1680 id : <id_given_in_input>
1680 id : <id_given_in_input>
1681 result : {
1681 result : {
1682 "msg": "Pulled from `<repository name>`"
1682 "msg": "Pulled from `<repository name>`"
1683 "repository": "<repository name>"
1683 "repository": "<repository name>"
1684 }
1684 }
1685 error : null
1685 error : null
1686
1686
1687 Example error output:
1687 Example error output:
1688
1688
1689 .. code-block:: bash
1689 .. code-block:: bash
1690
1690
1691 id : <id_given_in_input>
1691 id : <id_given_in_input>
1692 result : null
1692 result : null
1693 error : {
1693 error : {
1694 "Unable to pull changes from `<reponame>`"
1694 "Unable to pull changes from `<reponame>`"
1695 }
1695 }
1696
1696
1697 """
1697 """
1698
1698
1699 repo = get_repo_or_error(repoid)
1699 repo = get_repo_or_error(repoid)
1700 if not has_superadmin_permission(apiuser):
1700 if not has_superadmin_permission(apiuser):
1701 _perms = ('repository.admin',)
1701 _perms = ('repository.admin',)
1702 has_repo_permissions(apiuser, repoid, repo, _perms)
1702 has_repo_permissions(apiuser, repoid, repo, _perms)
1703
1703
1704 try:
1704 try:
1705 ScmModel().pull_changes(repo.repo_name, apiuser.username)
1705 ScmModel().pull_changes(repo.repo_name, apiuser.username)
1706 return {
1706 return {
1707 'msg': 'Pulled from `%s`' % repo.repo_name,
1707 'msg': 'Pulled from `%s`' % repo.repo_name,
1708 'repository': repo.repo_name
1708 'repository': repo.repo_name
1709 }
1709 }
1710 except Exception:
1710 except Exception:
1711 log.exception("Exception occurred while trying to "
1711 log.exception("Exception occurred while trying to "
1712 "pull changes from remote location")
1712 "pull changes from remote location")
1713 raise JSONRPCError(
1713 raise JSONRPCError(
1714 'Unable to pull changes from `%s`' % repo.repo_name
1714 'Unable to pull changes from `%s`' % repo.repo_name
1715 )
1715 )
1716
1716
1717
1717
1718 @jsonrpc_method()
1718 @jsonrpc_method()
1719 def strip(request, apiuser, repoid, revision, branch):
1719 def strip(request, apiuser, repoid, revision, branch):
1720 """
1720 """
1721 Strips the given revision from the specified repository.
1721 Strips the given revision from the specified repository.
1722
1722
1723 * This will remove the revision and all of its decendants.
1723 * This will remove the revision and all of its decendants.
1724
1724
1725 This command can only be run using an |authtoken| with admin rights to
1725 This command can only be run using an |authtoken| with admin rights to
1726 the specified repository.
1726 the specified repository.
1727
1727
1728 This command takes the following options:
1728 This command takes the following options:
1729
1729
1730 :param apiuser: This is filled automatically from the |authtoken|.
1730 :param apiuser: This is filled automatically from the |authtoken|.
1731 :type apiuser: AuthUser
1731 :type apiuser: AuthUser
1732 :param repoid: The repository name or repository ID.
1732 :param repoid: The repository name or repository ID.
1733 :type repoid: str or int
1733 :type repoid: str or int
1734 :param revision: The revision you wish to strip.
1734 :param revision: The revision you wish to strip.
1735 :type revision: str
1735 :type revision: str
1736 :param branch: The branch from which to strip the revision.
1736 :param branch: The branch from which to strip the revision.
1737 :type branch: str
1737 :type branch: str
1738
1738
1739 Example output:
1739 Example output:
1740
1740
1741 .. code-block:: bash
1741 .. code-block:: bash
1742
1742
1743 id : <id_given_in_input>
1743 id : <id_given_in_input>
1744 result : {
1744 result : {
1745 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
1745 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
1746 "repository": "<repository name>"
1746 "repository": "<repository name>"
1747 }
1747 }
1748 error : null
1748 error : null
1749
1749
1750 Example error output:
1750 Example error output:
1751
1751
1752 .. code-block:: bash
1752 .. code-block:: bash
1753
1753
1754 id : <id_given_in_input>
1754 id : <id_given_in_input>
1755 result : null
1755 result : null
1756 error : {
1756 error : {
1757 "Unable to strip commit <commit_hash> from repo `<repository name>`"
1757 "Unable to strip commit <commit_hash> from repo `<repository name>`"
1758 }
1758 }
1759
1759
1760 """
1760 """
1761
1761
1762 repo = get_repo_or_error(repoid)
1762 repo = get_repo_or_error(repoid)
1763 if not has_superadmin_permission(apiuser):
1763 if not has_superadmin_permission(apiuser):
1764 _perms = ('repository.admin',)
1764 _perms = ('repository.admin',)
1765 has_repo_permissions(apiuser, repoid, repo, _perms)
1765 has_repo_permissions(apiuser, repoid, repo, _perms)
1766
1766
1767 try:
1767 try:
1768 ScmModel().strip(repo, revision, branch)
1768 ScmModel().strip(repo, revision, branch)
1769 return {
1769 return {
1770 'msg': 'Stripped commit %s from repo `%s`' % (
1770 'msg': 'Stripped commit %s from repo `%s`' % (
1771 revision, repo.repo_name),
1771 revision, repo.repo_name),
1772 'repository': repo.repo_name
1772 'repository': repo.repo_name
1773 }
1773 }
1774 except Exception:
1774 except Exception:
1775 log.exception("Exception while trying to strip")
1775 log.exception("Exception while trying to strip")
1776 raise JSONRPCError(
1776 raise JSONRPCError(
1777 'Unable to strip commit %s from repo `%s`' % (
1777 'Unable to strip commit %s from repo `%s`' % (
1778 revision, repo.repo_name)
1778 revision, repo.repo_name)
1779 )
1779 )
1780
1780
1781
1781
1782 @jsonrpc_method()
1782 @jsonrpc_method()
1783 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
1783 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
1784 """
1784 """
1785 Returns all settings for a repository. If key is given it only returns the
1785 Returns all settings for a repository. If key is given it only returns the
1786 setting identified by the key or null.
1786 setting identified by the key or null.
1787
1787
1788 :param apiuser: This is filled automatically from the |authtoken|.
1788 :param apiuser: This is filled automatically from the |authtoken|.
1789 :type apiuser: AuthUser
1789 :type apiuser: AuthUser
1790 :param repoid: The repository name or repository id.
1790 :param repoid: The repository name or repository id.
1791 :type repoid: str or int
1791 :type repoid: str or int
1792 :param key: Key of the setting to return.
1792 :param key: Key of the setting to return.
1793 :type: key: Optional(str)
1793 :type: key: Optional(str)
1794
1794
1795 Example output:
1795 Example output:
1796
1796
1797 .. code-block:: bash
1797 .. code-block:: bash
1798
1798
1799 {
1799 {
1800 "error": null,
1800 "error": null,
1801 "id": 237,
1801 "id": 237,
1802 "result": {
1802 "result": {
1803 "extensions_largefiles": true,
1803 "extensions_largefiles": true,
1804 "hooks_changegroup_push_logger": true,
1804 "hooks_changegroup_push_logger": true,
1805 "hooks_changegroup_repo_size": false,
1805 "hooks_changegroup_repo_size": false,
1806 "hooks_outgoing_pull_logger": true,
1806 "hooks_outgoing_pull_logger": true,
1807 "phases_publish": "True",
1807 "phases_publish": "True",
1808 "rhodecode_hg_use_rebase_for_merging": true,
1808 "rhodecode_hg_use_rebase_for_merging": true,
1809 "rhodecode_pr_merge_enabled": true,
1809 "rhodecode_pr_merge_enabled": true,
1810 "rhodecode_use_outdated_comments": true
1810 "rhodecode_use_outdated_comments": true
1811 }
1811 }
1812 }
1812 }
1813 """
1813 """
1814
1814
1815 # Restrict access to this api method to admins only.
1815 # Restrict access to this api method to admins only.
1816 if not has_superadmin_permission(apiuser):
1816 if not has_superadmin_permission(apiuser):
1817 raise JSONRPCForbidden()
1817 raise JSONRPCForbidden()
1818
1818
1819 try:
1819 try:
1820 repo = get_repo_or_error(repoid)
1820 repo = get_repo_or_error(repoid)
1821 settings_model = VcsSettingsModel(repo=repo)
1821 settings_model = VcsSettingsModel(repo=repo)
1822 settings = settings_model.get_global_settings()
1822 settings = settings_model.get_global_settings()
1823 settings.update(settings_model.get_repo_settings())
1823 settings.update(settings_model.get_repo_settings())
1824
1824
1825 # If only a single setting is requested fetch it from all settings.
1825 # If only a single setting is requested fetch it from all settings.
1826 key = Optional.extract(key)
1826 key = Optional.extract(key)
1827 if key is not None:
1827 if key is not None:
1828 settings = settings.get(key, None)
1828 settings = settings.get(key, None)
1829 except Exception:
1829 except Exception:
1830 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
1830 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
1831 log.exception(msg)
1831 log.exception(msg)
1832 raise JSONRPCError(msg)
1832 raise JSONRPCError(msg)
1833
1833
1834 return settings
1834 return settings
1835
1835
1836
1836
1837 @jsonrpc_method()
1837 @jsonrpc_method()
1838 def set_repo_settings(request, apiuser, repoid, settings):
1838 def set_repo_settings(request, apiuser, repoid, settings):
1839 """
1839 """
1840 Update repository settings. Returns true on success.
1840 Update repository settings. Returns true on success.
1841
1841
1842 :param apiuser: This is filled automatically from the |authtoken|.
1842 :param apiuser: This is filled automatically from the |authtoken|.
1843 :type apiuser: AuthUser
1843 :type apiuser: AuthUser
1844 :param repoid: The repository name or repository id.
1844 :param repoid: The repository name or repository id.
1845 :type repoid: str or int
1845 :type repoid: str or int
1846 :param settings: The new settings for the repository.
1846 :param settings: The new settings for the repository.
1847 :type: settings: dict
1847 :type: settings: dict
1848
1848
1849 Example output:
1849 Example output:
1850
1850
1851 .. code-block:: bash
1851 .. code-block:: bash
1852
1852
1853 {
1853 {
1854 "error": null,
1854 "error": null,
1855 "id": 237,
1855 "id": 237,
1856 "result": true
1856 "result": true
1857 }
1857 }
1858 """
1858 """
1859 # Restrict access to this api method to admins only.
1859 # Restrict access to this api method to admins only.
1860 if not has_superadmin_permission(apiuser):
1860 if not has_superadmin_permission(apiuser):
1861 raise JSONRPCForbidden()
1861 raise JSONRPCForbidden()
1862
1862
1863 if type(settings) is not dict:
1863 if type(settings) is not dict:
1864 raise JSONRPCError('Settings have to be a JSON Object.')
1864 raise JSONRPCError('Settings have to be a JSON Object.')
1865
1865
1866 try:
1866 try:
1867 settings_model = VcsSettingsModel(repo=repoid)
1867 settings_model = VcsSettingsModel(repo=repoid)
1868
1868
1869 # Merge global, repo and incoming settings.
1869 # Merge global, repo and incoming settings.
1870 new_settings = settings_model.get_global_settings()
1870 new_settings = settings_model.get_global_settings()
1871 new_settings.update(settings_model.get_repo_settings())
1871 new_settings.update(settings_model.get_repo_settings())
1872 new_settings.update(settings)
1872 new_settings.update(settings)
1873
1873
1874 # Update the settings.
1874 # Update the settings.
1875 inherit_global_settings = new_settings.get(
1875 inherit_global_settings = new_settings.get(
1876 'inherit_global_settings', False)
1876 'inherit_global_settings', False)
1877 settings_model.create_or_update_repo_settings(
1877 settings_model.create_or_update_repo_settings(
1878 new_settings, inherit_global_settings=inherit_global_settings)
1878 new_settings, inherit_global_settings=inherit_global_settings)
1879 Session().commit()
1879 Session().commit()
1880 except Exception:
1880 except Exception:
1881 msg = 'Failed to update settings for repository `{}`'.format(repoid)
1881 msg = 'Failed to update settings for repository `{}`'.format(repoid)
1882 log.exception(msg)
1882 log.exception(msg)
1883 raise JSONRPCError(msg)
1883 raise JSONRPCError(msg)
1884
1884
1885 # Indicate success.
1885 # Indicate success.
1886 return True
1886 return True
@@ -1,699 +1,699 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2016 RhodeCode GmbH
3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23
23
24 import colander
24 import colander
25
25
26 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCForbidden
26 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCForbidden
27 from rhodecode.api.utils import (
27 from rhodecode.api.utils import (
28 has_superadmin_permission, Optional, OAttr, get_user_or_error,
28 has_superadmin_permission, Optional, OAttr, get_user_or_error,
29 store_update, get_repo_group_or_error,
29 store_update, get_repo_group_or_error,
30 get_perm_or_error, get_user_group_or_error, get_origin)
30 get_perm_or_error, get_user_group_or_error, get_origin)
31 from rhodecode.lib.auth import (
31 from rhodecode.lib.auth import (
32 HasPermissionAnyApi, HasRepoGroupPermissionAnyApi,
32 HasPermissionAnyApi, HasRepoGroupPermissionAnyApi,
33 HasUserGroupPermissionAnyApi)
33 HasUserGroupPermissionAnyApi)
34 from rhodecode.model.db import Session, RepoGroup
34 from rhodecode.model.db import Session, RepoGroup
35 from rhodecode.model.repo_group import RepoGroupModel
35 from rhodecode.model.repo_group import RepoGroupModel
36 from rhodecode.model.scm import RepoGroupList
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 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 @jsonrpc_method()
43 @jsonrpc_method()
44 def get_repo_group(request, apiuser, repogroupid):
44 def get_repo_group(request, apiuser, repogroupid):
45 """
45 """
46 Return the specified |repo| group, along with permissions,
46 Return the specified |repo| group, along with permissions,
47 and repositories inside the group
47 and repositories inside the group
48
48
49 :param apiuser: This is filled automatically from the |authtoken|.
49 :param apiuser: This is filled automatically from the |authtoken|.
50 :type apiuser: AuthUser
50 :type apiuser: AuthUser
51 :param repogroupid: Specify the name of ID of the repository group.
51 :param repogroupid: Specify the name of ID of the repository group.
52 :type repogroupid: str or int
52 :type repogroupid: str or int
53
53
54
54
55 Example output:
55 Example output:
56
56
57 .. code-block:: bash
57 .. code-block:: bash
58
58
59 {
59 {
60 "error": null,
60 "error": null,
61 "id": repo-group-id,
61 "id": repo-group-id,
62 "result": {
62 "result": {
63 "group_description": "repo group description",
63 "group_description": "repo group description",
64 "group_id": 14,
64 "group_id": 14,
65 "group_name": "group name",
65 "group_name": "group name",
66 "members": [
66 "members": [
67 {
67 {
68 "name": "super-admin-username",
68 "name": "super-admin-username",
69 "origin": "super-admin",
69 "origin": "super-admin",
70 "permission": "group.admin",
70 "permission": "group.admin",
71 "type": "user"
71 "type": "user"
72 },
72 },
73 {
73 {
74 "name": "owner-name",
74 "name": "owner-name",
75 "origin": "owner",
75 "origin": "owner",
76 "permission": "group.admin",
76 "permission": "group.admin",
77 "type": "user"
77 "type": "user"
78 },
78 },
79 {
79 {
80 "name": "user-group-name",
80 "name": "user-group-name",
81 "origin": "permission",
81 "origin": "permission",
82 "permission": "group.write",
82 "permission": "group.write",
83 "type": "user_group"
83 "type": "user_group"
84 }
84 }
85 ],
85 ],
86 "owner": "owner-name",
86 "owner": "owner-name",
87 "parent_group": null,
87 "parent_group": null,
88 "repositories": [ repo-list ]
88 "repositories": [ repo-list ]
89 }
89 }
90 }
90 }
91 """
91 """
92
92
93 repo_group = get_repo_group_or_error(repogroupid)
93 repo_group = get_repo_group_or_error(repogroupid)
94 if not has_superadmin_permission(apiuser):
94 if not has_superadmin_permission(apiuser):
95 # check if we have at least read permission for this repo group !
95 # check if we have at least read permission for this repo group !
96 _perms = ('group.admin', 'group.write', 'group.read',)
96 _perms = ('group.admin', 'group.write', 'group.read',)
97 if not HasRepoGroupPermissionAnyApi(*_perms)(
97 if not HasRepoGroupPermissionAnyApi(*_perms)(
98 user=apiuser, group_name=repo_group.group_name):
98 user=apiuser, group_name=repo_group.group_name):
99 raise JSONRPCError(
99 raise JSONRPCError(
100 'repository group `%s` does not exist' % (repogroupid,))
100 'repository group `%s` does not exist' % (repogroupid,))
101
101
102 permissions = []
102 permissions = []
103 for _user in repo_group.permissions():
103 for _user in repo_group.permissions():
104 user_data = {
104 user_data = {
105 'name': _user.username,
105 'name': _user.username,
106 'permission': _user.permission,
106 'permission': _user.permission,
107 'origin': get_origin(_user),
107 'origin': get_origin(_user),
108 'type': "user",
108 'type': "user",
109 }
109 }
110 permissions.append(user_data)
110 permissions.append(user_data)
111
111
112 for _user_group in repo_group.permission_user_groups():
112 for _user_group in repo_group.permission_user_groups():
113 user_group_data = {
113 user_group_data = {
114 'name': _user_group.users_group_name,
114 'name': _user_group.users_group_name,
115 'permission': _user_group.permission,
115 'permission': _user_group.permission,
116 'origin': get_origin(_user_group),
116 'origin': get_origin(_user_group),
117 'type': "user_group",
117 'type': "user_group",
118 }
118 }
119 permissions.append(user_group_data)
119 permissions.append(user_group_data)
120
120
121 data = repo_group.get_api_data()
121 data = repo_group.get_api_data()
122 data["members"] = permissions # TODO: this should be named permissions
122 data["members"] = permissions # TODO: this should be named permissions
123 return data
123 return data
124
124
125
125
126 @jsonrpc_method()
126 @jsonrpc_method()
127 def get_repo_groups(request, apiuser):
127 def get_repo_groups(request, apiuser):
128 """
128 """
129 Returns all repository groups.
129 Returns all repository groups.
130
130
131 :param apiuser: This is filled automatically from the |authtoken|.
131 :param apiuser: This is filled automatically from the |authtoken|.
132 :type apiuser: AuthUser
132 :type apiuser: AuthUser
133 """
133 """
134
134
135 result = []
135 result = []
136 _perms = ('group.read', 'group.write', 'group.admin',)
136 _perms = ('group.read', 'group.write', 'group.admin',)
137 extras = {'user': apiuser}
137 extras = {'user': apiuser}
138 for repo_group in RepoGroupList(RepoGroupModel().get_all(),
138 for repo_group in RepoGroupList(RepoGroupModel().get_all(),
139 perm_set=_perms, extra_kwargs=extras):
139 perm_set=_perms, extra_kwargs=extras):
140 result.append(repo_group.get_api_data())
140 result.append(repo_group.get_api_data())
141 return result
141 return result
142
142
143
143
144 @jsonrpc_method()
144 @jsonrpc_method()
145 def create_repo_group(request, apiuser, group_name, description=Optional(''),
145 def create_repo_group(request, apiuser, group_name, description=Optional(''),
146 owner=Optional(OAttr('apiuser')),
146 owner=Optional(OAttr('apiuser')),
147 copy_permissions=Optional(False)):
147 copy_permissions=Optional(False)):
148 """
148 """
149 Creates a repository group.
149 Creates a repository group.
150
150
151 * If the repository group name contains "/", all the required repository
151 * If the repository group name contains "/", all the required repository
152 groups will be created.
152 groups will be created.
153
153
154 For example "foo/bar/baz" will create |repo| groups "foo" and "bar"
154 For example "foo/bar/baz" will create |repo| groups "foo" and "bar"
155 (with "foo" as parent). It will also create the "baz" repository
155 (with "foo" as parent). It will also create the "baz" repository
156 with "bar" as |repo| group.
156 with "bar" as |repo| group.
157
157
158 This command can only be run using an |authtoken| with admin
158 This command can only be run using an |authtoken| with admin
159 permissions.
159 permissions.
160
160
161 :param apiuser: This is filled automatically from the |authtoken|.
161 :param apiuser: This is filled automatically from the |authtoken|.
162 :type apiuser: AuthUser
162 :type apiuser: AuthUser
163 :param group_name: Set the repository group name.
163 :param group_name: Set the repository group name.
164 :type group_name: str
164 :type group_name: str
165 :param description: Set the |repo| group description.
165 :param description: Set the |repo| group description.
166 :type description: str
166 :type description: str
167 :param owner: Set the |repo| group owner.
167 :param owner: Set the |repo| group owner.
168 :type owner: str
168 :type owner: str
169 :param copy_permissions:
169 :param copy_permissions:
170 :type copy_permissions:
170 :type copy_permissions:
171
171
172 Example output:
172 Example output:
173
173
174 .. code-block:: bash
174 .. code-block:: bash
175
175
176 id : <id_given_in_input>
176 id : <id_given_in_input>
177 result : {
177 result : {
178 "msg": "Created new repo group `<repo_group_name>`"
178 "msg": "Created new repo group `<repo_group_name>`"
179 "repo_group": <repogroup_object>
179 "repo_group": <repogroup_object>
180 }
180 }
181 error : null
181 error : null
182
182
183
183
184 Example error output:
184 Example error output:
185
185
186 .. code-block:: bash
186 .. code-block:: bash
187
187
188 id : <id_given_in_input>
188 id : <id_given_in_input>
189 result : null
189 result : null
190 error : {
190 error : {
191 failed to create repo group `<repogroupid>`
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 try:
197 try:
198 data = schema.deserialize({
198 data = schema.deserialize({
199 'group_name': group_name
199 'group_name': group_name
200 })
200 })
201 except colander.Invalid as e:
201 except colander.Invalid as e:
202 raise JSONRPCError("Validation failed: %s" % (e.asdict(),))
202 raise JSONRPCError("Validation failed: %s" % (e.asdict(),))
203 group_name = data['group_name']
203 group_name = data['group_name']
204
204
205 if isinstance(owner, Optional):
205 if isinstance(owner, Optional):
206 owner = apiuser.user_id
206 owner = apiuser.user_id
207
207
208 group_description = Optional.extract(description)
208 group_description = Optional.extract(description)
209 copy_permissions = Optional.extract(copy_permissions)
209 copy_permissions = Optional.extract(copy_permissions)
210
210
211 # get by full name with parents, check if it already exist
211 # get by full name with parents, check if it already exist
212 if RepoGroup.get_by_group_name(group_name):
212 if RepoGroup.get_by_group_name(group_name):
213 raise JSONRPCError("repo group `%s` already exist" % (group_name,))
213 raise JSONRPCError("repo group `%s` already exist" % (group_name,))
214
214
215 (group_name_cleaned,
215 (group_name_cleaned,
216 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(
216 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(
217 group_name)
217 group_name)
218
218
219 parent_group = None
219 parent_group = None
220 if parent_group_name:
220 if parent_group_name:
221 parent_group = get_repo_group_or_error(parent_group_name)
221 parent_group = get_repo_group_or_error(parent_group_name)
222
222
223 if not HasPermissionAnyApi(
223 if not HasPermissionAnyApi(
224 'hg.admin', 'hg.repogroup.create.true')(user=apiuser):
224 'hg.admin', 'hg.repogroup.create.true')(user=apiuser):
225 # check if we have admin permission for this parent repo group !
225 # check if we have admin permission for this parent repo group !
226 # users without admin or hg.repogroup.create can only create other
226 # users without admin or hg.repogroup.create can only create other
227 # groups in groups they own so this is a required, but can be empty
227 # groups in groups they own so this is a required, but can be empty
228 parent_group = getattr(parent_group, 'group_name', '')
228 parent_group = getattr(parent_group, 'group_name', '')
229 _perms = ('group.admin',)
229 _perms = ('group.admin',)
230 if not HasRepoGroupPermissionAnyApi(*_perms)(
230 if not HasRepoGroupPermissionAnyApi(*_perms)(
231 user=apiuser, group_name=parent_group):
231 user=apiuser, group_name=parent_group):
232 raise JSONRPCForbidden()
232 raise JSONRPCForbidden()
233
233
234 try:
234 try:
235 repo_group = RepoGroupModel().create(
235 repo_group = RepoGroupModel().create(
236 group_name=group_name,
236 group_name=group_name,
237 group_description=group_description,
237 group_description=group_description,
238 owner=owner,
238 owner=owner,
239 copy_permissions=copy_permissions)
239 copy_permissions=copy_permissions)
240 Session().commit()
240 Session().commit()
241 return {
241 return {
242 'msg': 'Created new repo group `%s`' % group_name,
242 'msg': 'Created new repo group `%s`' % group_name,
243 'repo_group': repo_group.get_api_data()
243 'repo_group': repo_group.get_api_data()
244 }
244 }
245 except Exception:
245 except Exception:
246 log.exception("Exception occurred while trying create repo group")
246 log.exception("Exception occurred while trying create repo group")
247 raise JSONRPCError(
247 raise JSONRPCError(
248 'failed to create repo group `%s`' % (group_name,))
248 'failed to create repo group `%s`' % (group_name,))
249
249
250
250
251 @jsonrpc_method()
251 @jsonrpc_method()
252 def update_repo_group(
252 def update_repo_group(
253 request, apiuser, repogroupid, group_name=Optional(''),
253 request, apiuser, repogroupid, group_name=Optional(''),
254 description=Optional(''), owner=Optional(OAttr('apiuser')),
254 description=Optional(''), owner=Optional(OAttr('apiuser')),
255 parent=Optional(None), enable_locking=Optional(False)):
255 parent=Optional(None), enable_locking=Optional(False)):
256 """
256 """
257 Updates repository group with the details given.
257 Updates repository group with the details given.
258
258
259 This command can only be run using an |authtoken| with admin
259 This command can only be run using an |authtoken| with admin
260 permissions.
260 permissions.
261
261
262 :param apiuser: This is filled automatically from the |authtoken|.
262 :param apiuser: This is filled automatically from the |authtoken|.
263 :type apiuser: AuthUser
263 :type apiuser: AuthUser
264 :param repogroupid: Set the ID of repository group.
264 :param repogroupid: Set the ID of repository group.
265 :type repogroupid: str or int
265 :type repogroupid: str or int
266 :param group_name: Set the name of the |repo| group.
266 :param group_name: Set the name of the |repo| group.
267 :type group_name: str
267 :type group_name: str
268 :param description: Set a description for the group.
268 :param description: Set a description for the group.
269 :type description: str
269 :type description: str
270 :param owner: Set the |repo| group owner.
270 :param owner: Set the |repo| group owner.
271 :type owner: str
271 :type owner: str
272 :param parent: Set the |repo| group parent.
272 :param parent: Set the |repo| group parent.
273 :type parent: str or int
273 :type parent: str or int
274 :param enable_locking: Enable |repo| locking. The default is false.
274 :param enable_locking: Enable |repo| locking. The default is false.
275 :type enable_locking: bool
275 :type enable_locking: bool
276 """
276 """
277
277
278 repo_group = get_repo_group_or_error(repogroupid)
278 repo_group = get_repo_group_or_error(repogroupid)
279 if not has_superadmin_permission(apiuser):
279 if not has_superadmin_permission(apiuser):
280 # check if we have admin permission for this repo group !
280 # check if we have admin permission for this repo group !
281 _perms = ('group.admin',)
281 _perms = ('group.admin',)
282 if not HasRepoGroupPermissionAnyApi(*_perms)(
282 if not HasRepoGroupPermissionAnyApi(*_perms)(
283 user=apiuser, group_name=repo_group.group_name):
283 user=apiuser, group_name=repo_group.group_name):
284 raise JSONRPCError(
284 raise JSONRPCError(
285 'repository group `%s` does not exist' % (repogroupid,))
285 'repository group `%s` does not exist' % (repogroupid,))
286
286
287 updates = {}
287 updates = {}
288 try:
288 try:
289 store_update(updates, group_name, 'group_name')
289 store_update(updates, group_name, 'group_name')
290 store_update(updates, description, 'group_description')
290 store_update(updates, description, 'group_description')
291 store_update(updates, owner, 'user')
291 store_update(updates, owner, 'user')
292 store_update(updates, parent, 'group_parent_id')
292 store_update(updates, parent, 'group_parent_id')
293 store_update(updates, enable_locking, 'enable_locking')
293 store_update(updates, enable_locking, 'enable_locking')
294 repo_group = RepoGroupModel().update(repo_group, updates)
294 repo_group = RepoGroupModel().update(repo_group, updates)
295 Session().commit()
295 Session().commit()
296 return {
296 return {
297 'msg': 'updated repository group ID:%s %s' % (
297 'msg': 'updated repository group ID:%s %s' % (
298 repo_group.group_id, repo_group.group_name),
298 repo_group.group_id, repo_group.group_name),
299 'repo_group': repo_group.get_api_data()
299 'repo_group': repo_group.get_api_data()
300 }
300 }
301 except Exception:
301 except Exception:
302 log.exception("Exception occurred while trying update repo group")
302 log.exception("Exception occurred while trying update repo group")
303 raise JSONRPCError('failed to update repository group `%s`'
303 raise JSONRPCError('failed to update repository group `%s`'
304 % (repogroupid,))
304 % (repogroupid,))
305
305
306
306
307 @jsonrpc_method()
307 @jsonrpc_method()
308 def delete_repo_group(request, apiuser, repogroupid):
308 def delete_repo_group(request, apiuser, repogroupid):
309 """
309 """
310 Deletes a |repo| group.
310 Deletes a |repo| group.
311
311
312 :param apiuser: This is filled automatically from the |authtoken|.
312 :param apiuser: This is filled automatically from the |authtoken|.
313 :type apiuser: AuthUser
313 :type apiuser: AuthUser
314 :param repogroupid: Set the name or ID of repository group to be
314 :param repogroupid: Set the name or ID of repository group to be
315 deleted.
315 deleted.
316 :type repogroupid: str or int
316 :type repogroupid: str or int
317
317
318 Example output:
318 Example output:
319
319
320 .. code-block:: bash
320 .. code-block:: bash
321
321
322 id : <id_given_in_input>
322 id : <id_given_in_input>
323 result : {
323 result : {
324 'msg': 'deleted repo group ID:<repogroupid> <repogroupname>
324 'msg': 'deleted repo group ID:<repogroupid> <repogroupname>
325 'repo_group': null
325 'repo_group': null
326 }
326 }
327 error : null
327 error : null
328
328
329 Example error output:
329 Example error output:
330
330
331 .. code-block:: bash
331 .. code-block:: bash
332
332
333 id : <id_given_in_input>
333 id : <id_given_in_input>
334 result : null
334 result : null
335 error : {
335 error : {
336 "failed to delete repo group ID:<repogroupid> <repogroupname>"
336 "failed to delete repo group ID:<repogroupid> <repogroupname>"
337 }
337 }
338
338
339 """
339 """
340
340
341 repo_group = get_repo_group_or_error(repogroupid)
341 repo_group = get_repo_group_or_error(repogroupid)
342 if not has_superadmin_permission(apiuser):
342 if not has_superadmin_permission(apiuser):
343 # check if we have admin permission for this repo group !
343 # check if we have admin permission for this repo group !
344 _perms = ('group.admin',)
344 _perms = ('group.admin',)
345 if not HasRepoGroupPermissionAnyApi(*_perms)(
345 if not HasRepoGroupPermissionAnyApi(*_perms)(
346 user=apiuser, group_name=repo_group.group_name):
346 user=apiuser, group_name=repo_group.group_name):
347 raise JSONRPCError(
347 raise JSONRPCError(
348 'repository group `%s` does not exist' % (repogroupid,))
348 'repository group `%s` does not exist' % (repogroupid,))
349 try:
349 try:
350 RepoGroupModel().delete(repo_group)
350 RepoGroupModel().delete(repo_group)
351 Session().commit()
351 Session().commit()
352 return {
352 return {
353 'msg': 'deleted repo group ID:%s %s' %
353 'msg': 'deleted repo group ID:%s %s' %
354 (repo_group.group_id, repo_group.group_name),
354 (repo_group.group_id, repo_group.group_name),
355 'repo_group': None
355 'repo_group': None
356 }
356 }
357 except Exception:
357 except Exception:
358 log.exception("Exception occurred while trying to delete repo group")
358 log.exception("Exception occurred while trying to delete repo group")
359 raise JSONRPCError('failed to delete repo group ID:%s %s' %
359 raise JSONRPCError('failed to delete repo group ID:%s %s' %
360 (repo_group.group_id, repo_group.group_name))
360 (repo_group.group_id, repo_group.group_name))
361
361
362
362
363 @jsonrpc_method()
363 @jsonrpc_method()
364 def grant_user_permission_to_repo_group(
364 def grant_user_permission_to_repo_group(
365 request, apiuser, repogroupid, userid, perm,
365 request, apiuser, repogroupid, userid, perm,
366 apply_to_children=Optional('none')):
366 apply_to_children=Optional('none')):
367 """
367 """
368 Grant permission for a user on the given repository group, or update
368 Grant permission for a user on the given repository group, or update
369 existing permissions if found.
369 existing permissions if found.
370
370
371 This command can only be run using an |authtoken| with admin
371 This command can only be run using an |authtoken| with admin
372 permissions.
372 permissions.
373
373
374 :param apiuser: This is filled automatically from the |authtoken|.
374 :param apiuser: This is filled automatically from the |authtoken|.
375 :type apiuser: AuthUser
375 :type apiuser: AuthUser
376 :param repogroupid: Set the name or ID of repository group.
376 :param repogroupid: Set the name or ID of repository group.
377 :type repogroupid: str or int
377 :type repogroupid: str or int
378 :param userid: Set the user name.
378 :param userid: Set the user name.
379 :type userid: str
379 :type userid: str
380 :param perm: (group.(none|read|write|admin))
380 :param perm: (group.(none|read|write|admin))
381 :type perm: str
381 :type perm: str
382 :param apply_to_children: 'none', 'repos', 'groups', 'all'
382 :param apply_to_children: 'none', 'repos', 'groups', 'all'
383 :type apply_to_children: str
383 :type apply_to_children: str
384
384
385 Example output:
385 Example output:
386
386
387 .. code-block:: bash
387 .. code-block:: bash
388
388
389 id : <id_given_in_input>
389 id : <id_given_in_input>
390 result: {
390 result: {
391 "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`",
391 "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`",
392 "success": true
392 "success": true
393 }
393 }
394 error: null
394 error: null
395
395
396 Example error output:
396 Example error output:
397
397
398 .. code-block:: bash
398 .. code-block:: bash
399
399
400 id : <id_given_in_input>
400 id : <id_given_in_input>
401 result : null
401 result : null
402 error : {
402 error : {
403 "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`"
403 "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`"
404 }
404 }
405
405
406 """
406 """
407
407
408 repo_group = get_repo_group_or_error(repogroupid)
408 repo_group = get_repo_group_or_error(repogroupid)
409
409
410 if not has_superadmin_permission(apiuser):
410 if not has_superadmin_permission(apiuser):
411 # check if we have admin permission for this repo group !
411 # check if we have admin permission for this repo group !
412 _perms = ('group.admin',)
412 _perms = ('group.admin',)
413 if not HasRepoGroupPermissionAnyApi(*_perms)(
413 if not HasRepoGroupPermissionAnyApi(*_perms)(
414 user=apiuser, group_name=repo_group.group_name):
414 user=apiuser, group_name=repo_group.group_name):
415 raise JSONRPCError(
415 raise JSONRPCError(
416 'repository group `%s` does not exist' % (repogroupid,))
416 'repository group `%s` does not exist' % (repogroupid,))
417
417
418 user = get_user_or_error(userid)
418 user = get_user_or_error(userid)
419 perm = get_perm_or_error(perm, prefix='group.')
419 perm = get_perm_or_error(perm, prefix='group.')
420 apply_to_children = Optional.extract(apply_to_children)
420 apply_to_children = Optional.extract(apply_to_children)
421
421
422 perm_additions = [[user.user_id, perm, "user"]]
422 perm_additions = [[user.user_id, perm, "user"]]
423 try:
423 try:
424 RepoGroupModel().update_permissions(repo_group=repo_group,
424 RepoGroupModel().update_permissions(repo_group=repo_group,
425 perm_additions=perm_additions,
425 perm_additions=perm_additions,
426 recursive=apply_to_children,
426 recursive=apply_to_children,
427 cur_user=apiuser)
427 cur_user=apiuser)
428 Session().commit()
428 Session().commit()
429 return {
429 return {
430 'msg': 'Granted perm: `%s` (recursive:%s) for user: '
430 'msg': 'Granted perm: `%s` (recursive:%s) for user: '
431 '`%s` in repo group: `%s`' % (
431 '`%s` in repo group: `%s`' % (
432 perm.permission_name, apply_to_children, user.username,
432 perm.permission_name, apply_to_children, user.username,
433 repo_group.name
433 repo_group.name
434 ),
434 ),
435 'success': True
435 'success': True
436 }
436 }
437 except Exception:
437 except Exception:
438 log.exception("Exception occurred while trying to grant "
438 log.exception("Exception occurred while trying to grant "
439 "user permissions to repo group")
439 "user permissions to repo group")
440 raise JSONRPCError(
440 raise JSONRPCError(
441 'failed to edit permission for user: '
441 'failed to edit permission for user: '
442 '`%s` in repo group: `%s`' % (userid, repo_group.name))
442 '`%s` in repo group: `%s`' % (userid, repo_group.name))
443
443
444
444
445 @jsonrpc_method()
445 @jsonrpc_method()
446 def revoke_user_permission_from_repo_group(
446 def revoke_user_permission_from_repo_group(
447 request, apiuser, repogroupid, userid,
447 request, apiuser, repogroupid, userid,
448 apply_to_children=Optional('none')):
448 apply_to_children=Optional('none')):
449 """
449 """
450 Revoke permission for a user in a given repository group.
450 Revoke permission for a user in a given repository group.
451
451
452 This command can only be run using an |authtoken| with admin
452 This command can only be run using an |authtoken| with admin
453 permissions on the |repo| group.
453 permissions on the |repo| group.
454
454
455 :param apiuser: This is filled automatically from the |authtoken|.
455 :param apiuser: This is filled automatically from the |authtoken|.
456 :type apiuser: AuthUser
456 :type apiuser: AuthUser
457 :param repogroupid: Set the name or ID of the repository group.
457 :param repogroupid: Set the name or ID of the repository group.
458 :type repogroupid: str or int
458 :type repogroupid: str or int
459 :param userid: Set the user name to revoke.
459 :param userid: Set the user name to revoke.
460 :type userid: str
460 :type userid: str
461 :param apply_to_children: 'none', 'repos', 'groups', 'all'
461 :param apply_to_children: 'none', 'repos', 'groups', 'all'
462 :type apply_to_children: str
462 :type apply_to_children: str
463
463
464 Example output:
464 Example output:
465
465
466 .. code-block:: bash
466 .. code-block:: bash
467
467
468 id : <id_given_in_input>
468 id : <id_given_in_input>
469 result: {
469 result: {
470 "msg" : "Revoked perm (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`",
470 "msg" : "Revoked perm (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`",
471 "success": true
471 "success": true
472 }
472 }
473 error: null
473 error: null
474
474
475 Example error output:
475 Example error output:
476
476
477 .. code-block:: bash
477 .. code-block:: bash
478
478
479 id : <id_given_in_input>
479 id : <id_given_in_input>
480 result : null
480 result : null
481 error : {
481 error : {
482 "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`"
482 "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`"
483 }
483 }
484
484
485 """
485 """
486
486
487 repo_group = get_repo_group_or_error(repogroupid)
487 repo_group = get_repo_group_or_error(repogroupid)
488
488
489 if not has_superadmin_permission(apiuser):
489 if not has_superadmin_permission(apiuser):
490 # check if we have admin permission for this repo group !
490 # check if we have admin permission for this repo group !
491 _perms = ('group.admin',)
491 _perms = ('group.admin',)
492 if not HasRepoGroupPermissionAnyApi(*_perms)(
492 if not HasRepoGroupPermissionAnyApi(*_perms)(
493 user=apiuser, group_name=repo_group.group_name):
493 user=apiuser, group_name=repo_group.group_name):
494 raise JSONRPCError(
494 raise JSONRPCError(
495 'repository group `%s` does not exist' % (repogroupid,))
495 'repository group `%s` does not exist' % (repogroupid,))
496
496
497 user = get_user_or_error(userid)
497 user = get_user_or_error(userid)
498 apply_to_children = Optional.extract(apply_to_children)
498 apply_to_children = Optional.extract(apply_to_children)
499
499
500 perm_deletions = [[user.user_id, None, "user"]]
500 perm_deletions = [[user.user_id, None, "user"]]
501 try:
501 try:
502 RepoGroupModel().update_permissions(repo_group=repo_group,
502 RepoGroupModel().update_permissions(repo_group=repo_group,
503 perm_deletions=perm_deletions,
503 perm_deletions=perm_deletions,
504 recursive=apply_to_children,
504 recursive=apply_to_children,
505 cur_user=apiuser)
505 cur_user=apiuser)
506 Session().commit()
506 Session().commit()
507 return {
507 return {
508 'msg': 'Revoked perm (recursive:%s) for user: '
508 'msg': 'Revoked perm (recursive:%s) for user: '
509 '`%s` in repo group: `%s`' % (
509 '`%s` in repo group: `%s`' % (
510 apply_to_children, user.username, repo_group.name
510 apply_to_children, user.username, repo_group.name
511 ),
511 ),
512 'success': True
512 'success': True
513 }
513 }
514 except Exception:
514 except Exception:
515 log.exception("Exception occurred while trying revoke user "
515 log.exception("Exception occurred while trying revoke user "
516 "permission from repo group")
516 "permission from repo group")
517 raise JSONRPCError(
517 raise JSONRPCError(
518 'failed to edit permission for user: '
518 'failed to edit permission for user: '
519 '`%s` in repo group: `%s`' % (userid, repo_group.name))
519 '`%s` in repo group: `%s`' % (userid, repo_group.name))
520
520
521
521
522 @jsonrpc_method()
522 @jsonrpc_method()
523 def grant_user_group_permission_to_repo_group(
523 def grant_user_group_permission_to_repo_group(
524 request, apiuser, repogroupid, usergroupid, perm,
524 request, apiuser, repogroupid, usergroupid, perm,
525 apply_to_children=Optional('none'), ):
525 apply_to_children=Optional('none'), ):
526 """
526 """
527 Grant permission for a user group on given repository group, or update
527 Grant permission for a user group on given repository group, or update
528 existing permissions if found.
528 existing permissions if found.
529
529
530 This command can only be run using an |authtoken| with admin
530 This command can only be run using an |authtoken| with admin
531 permissions on the |repo| group.
531 permissions on the |repo| group.
532
532
533 :param apiuser: This is filled automatically from the |authtoken|.
533 :param apiuser: This is filled automatically from the |authtoken|.
534 :type apiuser: AuthUser
534 :type apiuser: AuthUser
535 :param repogroupid: Set the name or id of repository group
535 :param repogroupid: Set the name or id of repository group
536 :type repogroupid: str or int
536 :type repogroupid: str or int
537 :param usergroupid: id of usergroup
537 :param usergroupid: id of usergroup
538 :type usergroupid: str or int
538 :type usergroupid: str or int
539 :param perm: (group.(none|read|write|admin))
539 :param perm: (group.(none|read|write|admin))
540 :type perm: str
540 :type perm: str
541 :param apply_to_children: 'none', 'repos', 'groups', 'all'
541 :param apply_to_children: 'none', 'repos', 'groups', 'all'
542 :type apply_to_children: str
542 :type apply_to_children: str
543
543
544 Example output:
544 Example output:
545
545
546 .. code-block:: bash
546 .. code-block:: bash
547
547
548 id : <id_given_in_input>
548 id : <id_given_in_input>
549 result : {
549 result : {
550 "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`",
550 "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`",
551 "success": true
551 "success": true
552
552
553 }
553 }
554 error : null
554 error : null
555
555
556 Example error output:
556 Example error output:
557
557
558 .. code-block:: bash
558 .. code-block:: bash
559
559
560 id : <id_given_in_input>
560 id : <id_given_in_input>
561 result : null
561 result : null
562 error : {
562 error : {
563 "failed to edit permission for user group: `<usergroup>` in repo group: `<repo_group_name>`"
563 "failed to edit permission for user group: `<usergroup>` in repo group: `<repo_group_name>`"
564 }
564 }
565
565
566 """
566 """
567
567
568 repo_group = get_repo_group_or_error(repogroupid)
568 repo_group = get_repo_group_or_error(repogroupid)
569 perm = get_perm_or_error(perm, prefix='group.')
569 perm = get_perm_or_error(perm, prefix='group.')
570 user_group = get_user_group_or_error(usergroupid)
570 user_group = get_user_group_or_error(usergroupid)
571 if not has_superadmin_permission(apiuser):
571 if not has_superadmin_permission(apiuser):
572 # check if we have admin permission for this repo group !
572 # check if we have admin permission for this repo group !
573 _perms = ('group.admin',)
573 _perms = ('group.admin',)
574 if not HasRepoGroupPermissionAnyApi(*_perms)(
574 if not HasRepoGroupPermissionAnyApi(*_perms)(
575 user=apiuser, group_name=repo_group.group_name):
575 user=apiuser, group_name=repo_group.group_name):
576 raise JSONRPCError(
576 raise JSONRPCError(
577 'repository group `%s` does not exist' % (repogroupid,))
577 'repository group `%s` does not exist' % (repogroupid,))
578
578
579 # check if we have at least read permission for this user group !
579 # check if we have at least read permission for this user group !
580 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
580 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
581 if not HasUserGroupPermissionAnyApi(*_perms)(
581 if not HasUserGroupPermissionAnyApi(*_perms)(
582 user=apiuser, user_group_name=user_group.users_group_name):
582 user=apiuser, user_group_name=user_group.users_group_name):
583 raise JSONRPCError(
583 raise JSONRPCError(
584 'user group `%s` does not exist' % (usergroupid,))
584 'user group `%s` does not exist' % (usergroupid,))
585
585
586 apply_to_children = Optional.extract(apply_to_children)
586 apply_to_children = Optional.extract(apply_to_children)
587
587
588 perm_additions = [[user_group.users_group_id, perm, "user_group"]]
588 perm_additions = [[user_group.users_group_id, perm, "user_group"]]
589 try:
589 try:
590 RepoGroupModel().update_permissions(repo_group=repo_group,
590 RepoGroupModel().update_permissions(repo_group=repo_group,
591 perm_additions=perm_additions,
591 perm_additions=perm_additions,
592 recursive=apply_to_children,
592 recursive=apply_to_children,
593 cur_user=apiuser)
593 cur_user=apiuser)
594 Session().commit()
594 Session().commit()
595 return {
595 return {
596 'msg': 'Granted perm: `%s` (recursive:%s) '
596 'msg': 'Granted perm: `%s` (recursive:%s) '
597 'for user group: `%s` in repo group: `%s`' % (
597 'for user group: `%s` in repo group: `%s`' % (
598 perm.permission_name, apply_to_children,
598 perm.permission_name, apply_to_children,
599 user_group.users_group_name, repo_group.name
599 user_group.users_group_name, repo_group.name
600 ),
600 ),
601 'success': True
601 'success': True
602 }
602 }
603 except Exception:
603 except Exception:
604 log.exception("Exception occurred while trying to grant user "
604 log.exception("Exception occurred while trying to grant user "
605 "group permissions to repo group")
605 "group permissions to repo group")
606 raise JSONRPCError(
606 raise JSONRPCError(
607 'failed to edit permission for user group: `%s` in '
607 'failed to edit permission for user group: `%s` in '
608 'repo group: `%s`' % (
608 'repo group: `%s`' % (
609 usergroupid, repo_group.name
609 usergroupid, repo_group.name
610 )
610 )
611 )
611 )
612
612
613
613
614 @jsonrpc_method()
614 @jsonrpc_method()
615 def revoke_user_group_permission_from_repo_group(
615 def revoke_user_group_permission_from_repo_group(
616 request, apiuser, repogroupid, usergroupid,
616 request, apiuser, repogroupid, usergroupid,
617 apply_to_children=Optional('none')):
617 apply_to_children=Optional('none')):
618 """
618 """
619 Revoke permission for user group on given repository.
619 Revoke permission for user group on given repository.
620
620
621 This command can only be run using an |authtoken| with admin
621 This command can only be run using an |authtoken| with admin
622 permissions on the |repo| group.
622 permissions on the |repo| group.
623
623
624 :param apiuser: This is filled automatically from the |authtoken|.
624 :param apiuser: This is filled automatically from the |authtoken|.
625 :type apiuser: AuthUser
625 :type apiuser: AuthUser
626 :param repogroupid: name or id of repository group
626 :param repogroupid: name or id of repository group
627 :type repogroupid: str or int
627 :type repogroupid: str or int
628 :param usergroupid:
628 :param usergroupid:
629 :param apply_to_children: 'none', 'repos', 'groups', 'all'
629 :param apply_to_children: 'none', 'repos', 'groups', 'all'
630 :type apply_to_children: str
630 :type apply_to_children: str
631
631
632 Example output:
632 Example output:
633
633
634 .. code-block:: bash
634 .. code-block:: bash
635
635
636 id : <id_given_in_input>
636 id : <id_given_in_input>
637 result: {
637 result: {
638 "msg" : "Revoked perm (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`",
638 "msg" : "Revoked perm (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`",
639 "success": true
639 "success": true
640 }
640 }
641 error: null
641 error: null
642
642
643 Example error output:
643 Example error output:
644
644
645 .. code-block:: bash
645 .. code-block:: bash
646
646
647 id : <id_given_in_input>
647 id : <id_given_in_input>
648 result : null
648 result : null
649 error : {
649 error : {
650 "failed to edit permission for user group: `<usergroup>` in repo group: `<repo_group_name>`"
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 repo_group = get_repo_group_or_error(repogroupid)
656 repo_group = get_repo_group_or_error(repogroupid)
657 user_group = get_user_group_or_error(usergroupid)
657 user_group = get_user_group_or_error(usergroupid)
658 if not has_superadmin_permission(apiuser):
658 if not has_superadmin_permission(apiuser):
659 # check if we have admin permission for this repo group !
659 # check if we have admin permission for this repo group !
660 _perms = ('group.admin',)
660 _perms = ('group.admin',)
661 if not HasRepoGroupPermissionAnyApi(*_perms)(
661 if not HasRepoGroupPermissionAnyApi(*_perms)(
662 user=apiuser, group_name=repo_group.group_name):
662 user=apiuser, group_name=repo_group.group_name):
663 raise JSONRPCError(
663 raise JSONRPCError(
664 'repository group `%s` does not exist' % (repogroupid,))
664 'repository group `%s` does not exist' % (repogroupid,))
665
665
666 # check if we have at least read permission for this user group !
666 # check if we have at least read permission for this user group !
667 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
667 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
668 if not HasUserGroupPermissionAnyApi(*_perms)(
668 if not HasUserGroupPermissionAnyApi(*_perms)(
669 user=apiuser, user_group_name=user_group.users_group_name):
669 user=apiuser, user_group_name=user_group.users_group_name):
670 raise JSONRPCError(
670 raise JSONRPCError(
671 'user group `%s` does not exist' % (usergroupid,))
671 'user group `%s` does not exist' % (usergroupid,))
672
672
673 apply_to_children = Optional.extract(apply_to_children)
673 apply_to_children = Optional.extract(apply_to_children)
674
674
675 perm_deletions = [[user_group.users_group_id, None, "user_group"]]
675 perm_deletions = [[user_group.users_group_id, None, "user_group"]]
676 try:
676 try:
677 RepoGroupModel().update_permissions(repo_group=repo_group,
677 RepoGroupModel().update_permissions(repo_group=repo_group,
678 perm_deletions=perm_deletions,
678 perm_deletions=perm_deletions,
679 recursive=apply_to_children,
679 recursive=apply_to_children,
680 cur_user=apiuser)
680 cur_user=apiuser)
681 Session().commit()
681 Session().commit()
682 return {
682 return {
683 'msg': 'Revoked perm (recursive:%s) for user group: '
683 'msg': 'Revoked perm (recursive:%s) for user group: '
684 '`%s` in repo group: `%s`' % (
684 '`%s` in repo group: `%s`' % (
685 apply_to_children, user_group.users_group_name,
685 apply_to_children, user_group.users_group_name,
686 repo_group.name
686 repo_group.name
687 ),
687 ),
688 'success': True
688 'success': True
689 }
689 }
690 except Exception:
690 except Exception:
691 log.exception("Exception occurred while trying revoke user group "
691 log.exception("Exception occurred while trying revoke user group "
692 "permissions from repo group")
692 "permissions from repo group")
693 raise JSONRPCError(
693 raise JSONRPCError(
694 'failed to edit permission for user group: '
694 'failed to edit permission for user group: '
695 '`%s` in repo group: `%s`' % (
695 '`%s` in repo group: `%s`' % (
696 user_group.users_group_name, repo_group.name
696 user_group.users_group_name, repo_group.name
697 )
697 )
698 )
698 )
699
699
@@ -1,339 +1,367 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2013-2016 RhodeCode GmbH
3 # Copyright (C) 2013-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 gist controller for RhodeCode
23 gist controller for RhodeCode
24 """
24 """
25
25
26 import time
26 import time
27 import logging
27 import logging
28 import traceback
28
29 import formencode
29 import formencode
30 import peppercorn
30 from formencode import htmlfill
31 from formencode import htmlfill
31
32
32 from pylons import request, response, tmpl_context as c, url
33 from pylons import request, response, tmpl_context as c, url
33 from pylons.controllers.util import abort, redirect
34 from pylons.controllers.util import abort, redirect
34 from pylons.i18n.translation import _
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 from rhodecode.model.gist import GistModel
40 from rhodecode.model.gist import GistModel
38 from rhodecode.model.meta import Session
41 from rhodecode.model.meta import Session
39 from rhodecode.model.db import Gist, User
42 from rhodecode.model.db import Gist, User
40 from rhodecode.lib import auth
43 from rhodecode.lib import auth
41 from rhodecode.lib import helpers as h
44 from rhodecode.lib import helpers as h
42 from rhodecode.lib.base import BaseController, render
45 from rhodecode.lib.base import BaseController, render
43 from rhodecode.lib.auth import LoginRequired, NotAnonymous
46 from rhodecode.lib.auth import LoginRequired, NotAnonymous
44 from rhodecode.lib.utils import jsonify
47 from rhodecode.lib.utils import jsonify
45 from rhodecode.lib.utils2 import safe_str, safe_int, time_to_datetime
48 from rhodecode.lib.utils2 import safe_str, safe_int, time_to_datetime
46 from rhodecode.lib.ext_json import json
49 from rhodecode.lib.ext_json import json
47 from webob.exc import HTTPNotFound, HTTPForbidden
48 from sqlalchemy.sql.expression import or_
49 from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError
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 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
52
56
53
57
54 class GistsController(BaseController):
58 class GistsController(BaseController):
55 """REST Controller styled on the Atom Publishing Protocol"""
59 """REST Controller styled on the Atom Publishing Protocol"""
56
60
57 def __load_defaults(self, extra_values=None):
61 def __load_defaults(self, extra_values=None):
58 c.lifetime_values = [
62 c.lifetime_values = [
59 (str(-1), _('forever')),
63 (-1, _('forever')),
60 (str(5), _('5 minutes')),
64 (5, _('5 minutes')),
61 (str(60), _('1 hour')),
65 (60, _('1 hour')),
62 (str(60 * 24), _('1 day')),
66 (60 * 24, _('1 day')),
63 (str(60 * 24 * 30), _('1 month')),
67 (60 * 24 * 30, _('1 month')),
64 ]
68 ]
65 if extra_values:
69 if extra_values:
66 c.lifetime_values.append(extra_values)
70 c.lifetime_values.append(extra_values)
67 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
71 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
68 c.acl_options = [
72 c.acl_options = [
69 (Gist.ACL_LEVEL_PRIVATE, _("Requires registered account")),
73 (Gist.ACL_LEVEL_PRIVATE, _("Requires registered account")),
70 (Gist.ACL_LEVEL_PUBLIC, _("Can be accessed by anonymous users"))
74 (Gist.ACL_LEVEL_PUBLIC, _("Can be accessed by anonymous users"))
71 ]
75 ]
72
76
73 @LoginRequired()
77 @LoginRequired()
74 def index(self):
78 def index(self):
75 """GET /admin/gists: All items in the collection"""
79 """GET /admin/gists: All items in the collection"""
76 # url('gists')
80 # url('gists')
77 not_default_user = c.rhodecode_user.username != User.DEFAULT_USER
81 not_default_user = c.rhodecode_user.username != User.DEFAULT_USER
78 c.show_private = request.GET.get('private') and not_default_user
82 c.show_private = request.GET.get('private') and not_default_user
79 c.show_public = request.GET.get('public') and not_default_user
83 c.show_public = request.GET.get('public') and not_default_user
80 c.show_all = request.GET.get('all') and c.rhodecode_user.admin
84 c.show_all = request.GET.get('all') and c.rhodecode_user.admin
81
85
82 gists = _gists = Gist().query()\
86 gists = _gists = Gist().query()\
83 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
87 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
84 .order_by(Gist.created_on.desc())
88 .order_by(Gist.created_on.desc())
85
89
86 c.active = 'public'
90 c.active = 'public'
87 # MY private
91 # MY private
88 if c.show_private and not c.show_public:
92 if c.show_private and not c.show_public:
89 gists = _gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
93 gists = _gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
90 .filter(Gist.gist_owner == c.rhodecode_user.user_id)
94 .filter(Gist.gist_owner == c.rhodecode_user.user_id)
91 c.active = 'my_private'
95 c.active = 'my_private'
92 # MY public
96 # MY public
93 elif c.show_public and not c.show_private:
97 elif c.show_public and not c.show_private:
94 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\
98 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\
95 .filter(Gist.gist_owner == c.rhodecode_user.user_id)
99 .filter(Gist.gist_owner == c.rhodecode_user.user_id)
96 c.active = 'my_public'
100 c.active = 'my_public'
97 # MY public+private
101 # MY public+private
98 elif c.show_private and c.show_public:
102 elif c.show_private and c.show_public:
99 gists = _gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
103 gists = _gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
100 Gist.gist_type == Gist.GIST_PRIVATE))\
104 Gist.gist_type == Gist.GIST_PRIVATE))\
101 .filter(Gist.gist_owner == c.rhodecode_user.user_id)
105 .filter(Gist.gist_owner == c.rhodecode_user.user_id)
102 c.active = 'my_all'
106 c.active = 'my_all'
103 # Show all by super-admin
107 # Show all by super-admin
104 elif c.show_all:
108 elif c.show_all:
105 c.active = 'all'
109 c.active = 'all'
106 gists = _gists
110 gists = _gists
107
111
108 # default show ALL public gists
112 # default show ALL public gists
109 if not c.show_public and not c.show_private and not c.show_all:
113 if not c.show_public and not c.show_private and not c.show_all:
110 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
114 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
111 c.active = 'public'
115 c.active = 'public'
112
116
113 from rhodecode.lib.utils import PartialRenderer
117 from rhodecode.lib.utils import PartialRenderer
114 _render = PartialRenderer('data_table/_dt_elements.html')
118 _render = PartialRenderer('data_table/_dt_elements.html')
115
119
116 data = []
120 data = []
117
121
118 for gist in gists:
122 for gist in gists:
119 data.append({
123 data.append({
120 'created_on': _render('gist_created', gist.created_on),
124 'created_on': _render('gist_created', gist.created_on),
121 'created_on_raw': gist.created_on,
125 'created_on_raw': gist.created_on,
122 'type': _render('gist_type', gist.gist_type),
126 'type': _render('gist_type', gist.gist_type),
123 'access_id': _render('gist_access_id', gist.gist_access_id, gist.owner.full_contact),
127 'access_id': _render('gist_access_id', gist.gist_access_id, gist.owner.full_contact),
124 'author': _render('gist_author', gist.owner.full_contact, gist.created_on, gist.gist_expires),
128 'author': _render('gist_author', gist.owner.full_contact, gist.created_on, gist.gist_expires),
125 'author_raw': h.escape(gist.owner.full_contact),
129 'author_raw': h.escape(gist.owner.full_contact),
126 'expires': _render('gist_expires', gist.gist_expires),
130 'expires': _render('gist_expires', gist.gist_expires),
127 'description': _render('gist_description', gist.gist_description)
131 'description': _render('gist_description', gist.gist_description)
128 })
132 })
129 c.data = json.dumps(data)
133 c.data = json.dumps(data)
130 return render('admin/gists/index.html')
134 return render('admin/gists/index.html')
131
135
132 @LoginRequired()
136 @LoginRequired()
133 @NotAnonymous()
137 @NotAnonymous()
134 @auth.CSRFRequired()
138 @auth.CSRFRequired()
135 def create(self):
139 def create(self):
136 """POST /admin/gists: Create a new item"""
140 """POST /admin/gists: Create a new item"""
137 # url('gists')
141 # url('gists')
138 self.__load_defaults()
142 self.__load_defaults()
139 gist_form = GistForm([x[0] for x in c.lifetime_values],
143
140 [x[0] for x in c.acl_options])()
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 try:
160 try:
142 form_result = gist_form.to_python(dict(request.POST))
161
143 # TODO: multiple files support, from the form
162 schema_data = schema.deserialize(data)
144 filename = form_result['filename'] or Gist.DEFAULT_FILENAME
163 # convert to safer format with just KEYs so we sure no duplicates
145 nodes = {
164 schema_data['nodes'] = gist_schema.sequence_to_nodes(
146 filename: {
165 schema_data['nodes'])
147 'content': form_result['content'],
166
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)
155 gist = GistModel().create(
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 owner=c.rhodecode_user.user_id,
170 owner=c.rhodecode_user.user_id,
158 gist_mapping=nodes,
171 gist_mapping=schema_data['nodes'],
159 gist_type=gist_type,
172 gist_type=schema_data['gist_type'],
160 lifetime=form_result['lifetime'],
173 lifetime=schema_data['lifetime'],
161 gist_id=form_result['gistid'],
174 gist_acl_level=schema_data['gist_acl_level']
162 gist_acl_level=gist_acl_level
163 )
175 )
164 Session().commit()
176 Session().commit()
165 new_gist_id = gist.gist_access_id
177 new_gist_id = gist.gist_access_id
166 except formencode.Invalid as errors:
178 except validation_schema.Invalid as errors:
167 defaults = errors.value
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 return formencode.htmlfill.render(
189 return formencode.htmlfill.render(
170 render('admin/gists/new.html'),
190 render('admin/gists/new.html'),
171 defaults=defaults,
191 defaults=defaults,
172 errors=errors.error_dict or {},
192 errors=errors,
173 prefix_error=False,
193 prefix_error=False,
174 encoding="UTF-8",
194 encoding="UTF-8",
175 force_defaults=False
195 force_defaults=False
176 )
196 )
177
197
178 except Exception:
198 except Exception:
179 log.exception("Exception while trying to create a gist")
199 log.exception("Exception while trying to create a gist")
180 h.flash(_('Error occurred during gist creation'), category='error')
200 h.flash(_('Error occurred during gist creation'), category='error')
181 return redirect(url('new_gist'))
201 return redirect(url('new_gist'))
182 return redirect(url('gist', gist_id=new_gist_id))
202 return redirect(url('gist', gist_id=new_gist_id))
183
203
184 @LoginRequired()
204 @LoginRequired()
185 @NotAnonymous()
205 @NotAnonymous()
186 def new(self, format='html'):
206 def new(self, format='html'):
187 """GET /admin/gists/new: Form to create a new item"""
207 """GET /admin/gists/new: Form to create a new item"""
188 # url('new_gist')
208 # url('new_gist')
189 self.__load_defaults()
209 self.__load_defaults()
190 return render('admin/gists/new.html')
210 return render('admin/gists/new.html')
191
211
192 @LoginRequired()
212 @LoginRequired()
193 @NotAnonymous()
213 @NotAnonymous()
194 @auth.CSRFRequired()
214 @auth.CSRFRequired()
195 def delete(self, gist_id):
215 def delete(self, gist_id):
196 """DELETE /admin/gists/gist_id: Delete an existing item"""
216 """DELETE /admin/gists/gist_id: Delete an existing item"""
197 # Forms posted to this method should contain a hidden field:
217 # Forms posted to this method should contain a hidden field:
198 # <input type="hidden" name="_method" value="DELETE" />
218 # <input type="hidden" name="_method" value="DELETE" />
199 # Or using helpers:
219 # Or using helpers:
200 # h.form(url('gist', gist_id=ID),
220 # h.form(url('gist', gist_id=ID),
201 # method='delete')
221 # method='delete')
202 # url('gist', gist_id=ID)
222 # url('gist', gist_id=ID)
203 c.gist = Gist.get_or_404(gist_id)
223 c.gist = Gist.get_or_404(gist_id)
204
224
205 owner = c.gist.gist_owner == c.rhodecode_user.user_id
225 owner = c.gist.gist_owner == c.rhodecode_user.user_id
206 if not (h.HasPermissionAny('hg.admin')() or owner):
226 if not (h.HasPermissionAny('hg.admin')() or owner):
207 raise HTTPForbidden()
227 raise HTTPForbidden()
208
228
209 GistModel().delete(c.gist)
229 GistModel().delete(c.gist)
210 Session().commit()
230 Session().commit()
211 h.flash(_('Deleted gist %s') % c.gist.gist_access_id, category='success')
231 h.flash(_('Deleted gist %s') % c.gist.gist_access_id, category='success')
212
232
213 return redirect(url('gists'))
233 return redirect(url('gists'))
214
234
215 def _add_gist_to_context(self, gist_id):
235 def _add_gist_to_context(self, gist_id):
216 c.gist = Gist.get_or_404(gist_id)
236 c.gist = Gist.get_or_404(gist_id)
217
237
218 # Check if this gist is expired
238 # Check if this gist is expired
219 if c.gist.gist_expires != -1:
239 if c.gist.gist_expires != -1:
220 if time.time() > c.gist.gist_expires:
240 if time.time() > c.gist.gist_expires:
221 log.error(
241 log.error(
222 'Gist expired at %s', time_to_datetime(c.gist.gist_expires))
242 'Gist expired at %s', time_to_datetime(c.gist.gist_expires))
223 raise HTTPNotFound()
243 raise HTTPNotFound()
224
244
225 # check if this gist requires a login
245 # check if this gist requires a login
226 is_default_user = c.rhodecode_user.username == User.DEFAULT_USER
246 is_default_user = c.rhodecode_user.username == User.DEFAULT_USER
227 if c.gist.acl_level == Gist.ACL_LEVEL_PRIVATE and is_default_user:
247 if c.gist.acl_level == Gist.ACL_LEVEL_PRIVATE and is_default_user:
228 log.error("Anonymous user %s tried to access protected gist `%s`",
248 log.error("Anonymous user %s tried to access protected gist `%s`",
229 c.rhodecode_user, gist_id)
249 c.rhodecode_user, gist_id)
230 raise HTTPNotFound()
250 raise HTTPNotFound()
231
251
232 @LoginRequired()
252 @LoginRequired()
233 def show(self, gist_id, revision='tip', format='html', f_path=None):
253 def show(self, gist_id, revision='tip', format='html', f_path=None):
234 """GET /admin/gists/gist_id: Show a specific item"""
254 """GET /admin/gists/gist_id: Show a specific item"""
235 # url('gist', gist_id=ID)
255 # url('gist', gist_id=ID)
236 self._add_gist_to_context(gist_id)
256 self._add_gist_to_context(gist_id)
237 c.render = not request.GET.get('no-render', False)
257 c.render = not request.GET.get('no-render', False)
238
258
239 try:
259 try:
240 c.file_last_commit, c.files = GistModel().get_gist_files(
260 c.file_last_commit, c.files = GistModel().get_gist_files(
241 gist_id, revision=revision)
261 gist_id, revision=revision)
242 except VCSError:
262 except VCSError:
243 log.exception("Exception in gist show")
263 log.exception("Exception in gist show")
244 raise HTTPNotFound()
264 raise HTTPNotFound()
245 if format == 'raw':
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 response.content_type = 'text/plain'
268 response.content_type = 'text/plain'
248 return content
269 return content
249 return render('admin/gists/show.html')
270 return render('admin/gists/show.html')
250
271
251 @LoginRequired()
272 @LoginRequired()
252 @NotAnonymous()
273 @NotAnonymous()
253 @auth.CSRFRequired()
274 @auth.CSRFRequired()
254 def edit(self, gist_id):
275 def edit(self, gist_id):
276 self.__load_defaults()
255 self._add_gist_to_context(gist_id)
277 self._add_gist_to_context(gist_id)
256
278
257 owner = c.gist.gist_owner == c.rhodecode_user.user_id
279 owner = c.gist.gist_owner == c.rhodecode_user.user_id
258 if not (h.HasPermissionAny('hg.admin')() or owner):
280 if not (h.HasPermissionAny('hg.admin')() or owner):
259 raise HTTPForbidden()
281 raise HTTPForbidden()
260
282
261 rpost = request.POST
283 data = peppercorn.parse(request.POST.items())
262 nodes = {}
284
263 _file_data = zip(rpost.getall('org_files'), rpost.getall('files'),
285 schema = gist_schema.GistSchema()
264 rpost.getall('mimetypes'), rpost.getall('contents'))
286 schema = schema.bind(
265 for org_filename, filename, mimetype, content in _file_data:
287 # '0' is special value to leave lifetime untouched
266 nodes[org_filename] = {
288 lifetime_options=[x[0] for x in c.lifetime_values] + [0],
267 'org_filename': org_filename,
289 )
268 'filename': filename,
290
269 'content': content,
270 'lexer': mimetype,
271 }
272 try:
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 GistModel().update(
297 GistModel().update(
274 gist=c.gist,
298 gist=c.gist,
275 description=rpost['description'],
299 description=schema_data['description'],
276 owner=c.gist.owner,
300 owner=c.gist.owner,
277 gist_mapping=nodes,
301 gist_mapping=schema_data['nodes'],
278 gist_type=c.gist.gist_type,
302 gist_type=schema_data['gist_type'],
279 lifetime=rpost['lifetime'],
303 lifetime=schema_data['lifetime'],
280 gist_acl_level=rpost['acl_level']
304 gist_acl_level=schema_data['gist_acl_level']
281 )
305 )
282
306
283 Session().commit()
307 Session().commit()
284 h.flash(_('Successfully updated gist content'), category='success')
308 h.flash(_('Successfully updated gist content'), category='success')
285 except NodeNotChangedError:
309 except NodeNotChangedError:
286 # raised if nothing was changed in repo itself. We anyway then
310 # raised if nothing was changed in repo itself. We anyway then
287 # store only DB stuff for gist
311 # store only DB stuff for gist
288 Session().commit()
312 Session().commit()
289 h.flash(_('Successfully updated gist data'), category='success')
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 except Exception:
318 except Exception:
291 log.exception("Exception in gist edit")
319 log.exception("Exception in gist edit")
292 h.flash(_('Error occurred during update of gist %s') % gist_id,
320 h.flash(_('Error occurred during update of gist %s') % gist_id,
293 category='error')
321 category='error')
294
322
295 return redirect(url('gist', gist_id=gist_id))
323 return redirect(url('gist', gist_id=gist_id))
296
324
297 @LoginRequired()
325 @LoginRequired()
298 @NotAnonymous()
326 @NotAnonymous()
299 def edit_form(self, gist_id, format='html'):
327 def edit_form(self, gist_id, format='html'):
300 """GET /admin/gists/gist_id/edit: Form to edit an existing item"""
328 """GET /admin/gists/gist_id/edit: Form to edit an existing item"""
301 # url('edit_gist', gist_id=ID)
329 # url('edit_gist', gist_id=ID)
302 self._add_gist_to_context(gist_id)
330 self._add_gist_to_context(gist_id)
303
331
304 owner = c.gist.gist_owner == c.rhodecode_user.user_id
332 owner = c.gist.gist_owner == c.rhodecode_user.user_id
305 if not (h.HasPermissionAny('hg.admin')() or owner):
333 if not (h.HasPermissionAny('hg.admin')() or owner):
306 raise HTTPForbidden()
334 raise HTTPForbidden()
307
335
308 try:
336 try:
309 c.file_last_commit, c.files = GistModel().get_gist_files(gist_id)
337 c.file_last_commit, c.files = GistModel().get_gist_files(gist_id)
310 except VCSError:
338 except VCSError:
311 log.exception("Exception in gist edit")
339 log.exception("Exception in gist edit")
312 raise HTTPNotFound()
340 raise HTTPNotFound()
313
341
314 if c.gist.gist_expires == -1:
342 if c.gist.gist_expires == -1:
315 expiry = _('never')
343 expiry = _('never')
316 else:
344 else:
317 # this cannot use timeago, since it's used in select2 as a value
345 # this cannot use timeago, since it's used in select2 as a value
318 expiry = h.age(h.time_to_datetime(c.gist.gist_expires))
346 expiry = h.age(h.time_to_datetime(c.gist.gist_expires))
319 self.__load_defaults(
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 return render('admin/gists/edit.html')
349 return render('admin/gists/edit.html')
322
350
323 @LoginRequired()
351 @LoginRequired()
324 @NotAnonymous()
352 @NotAnonymous()
325 @jsonify
353 @jsonify
326 def check_revision(self, gist_id):
354 def check_revision(self, gist_id):
327 c.gist = Gist.get_or_404(gist_id)
355 c.gist = Gist.get_or_404(gist_id)
328 last_rev = c.gist.scm_instance().get_commit()
356 last_rev = c.gist.scm_instance().get_commit()
329 success = True
357 success = True
330 revision = request.GET.get('revision')
358 revision = request.GET.get('revision')
331
359
332 ##TODO: maybe move this to model ?
360 ##TODO: maybe move this to model ?
333 if revision != last_rev.raw_id:
361 if revision != last_rev.raw_id:
334 log.error('Last revision %s is different then submitted %s'
362 log.error('Last revision %s is different then submitted %s'
335 % (revision, last_rev))
363 % (revision, last_rev))
336 # our gist has newer version than we
364 # our gist has newer version than we
337 success = False
365 success = False
338
366
339 return {'success': success}
367 return {'success': success}
@@ -1,111 +1,111 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Search controller for RhodeCode
22 Search controller for RhodeCode
23 """
23 """
24
24
25 import logging
25 import logging
26 import urllib
26 import urllib
27
27
28 from pylons import request, config, tmpl_context as c
28 from pylons import request, config, tmpl_context as c
29
29
30 from webhelpers.util import update_params
30 from webhelpers.util import update_params
31
31
32 from rhodecode.lib.auth import LoginRequired, AuthUser
32 from rhodecode.lib.auth import LoginRequired, AuthUser
33 from rhodecode.lib.base import BaseRepoController, render
33 from rhodecode.lib.base import BaseRepoController, render
34 from rhodecode.lib.helpers import Page
34 from rhodecode.lib.helpers import Page
35 from rhodecode.lib.utils2 import safe_str, safe_int
35 from rhodecode.lib.utils2 import safe_str, safe_int
36 from rhodecode.lib.index import searcher_from_config
36 from rhodecode.lib.index import searcher_from_config
37 from rhodecode.model import validation_schema
37 from rhodecode.model import validation_schema
38 from rhodecode.model.validation_schema.schemas import search_schema
38
39
39 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
40
41
41
42
42 class SearchController(BaseRepoController):
43 class SearchController(BaseRepoController):
43
44
44 @LoginRequired()
45 @LoginRequired()
45 def index(self, repo_name=None):
46 def index(self, repo_name=None):
46
47
47 searcher = searcher_from_config(config)
48 searcher = searcher_from_config(config)
48 formatted_results = []
49 formatted_results = []
49 execution_time = ''
50 execution_time = ''
50
51
51 schema = validation_schema.SearchParamsSchema()
52 schema = search_schema.SearchParamsSchema()
52
53
53 search_params = {}
54 search_params = {}
54 errors = []
55 errors = []
55 try:
56 try:
56 search_params = schema.deserialize(
57 search_params = schema.deserialize(
57 dict(search_query=request.GET.get('q'),
58 dict(search_query=request.GET.get('q'),
58 search_type=request.GET.get('type'),
59 search_type=request.GET.get('type'),
59 search_sort=request.GET.get('sort'),
60 search_sort=request.GET.get('sort'),
60 page_limit=request.GET.get('page_limit'),
61 page_limit=request.GET.get('page_limit'),
61 requested_page=request.GET.get('page'))
62 requested_page=request.GET.get('page'))
62 )
63 )
63 except validation_schema.Invalid as e:
64 except validation_schema.Invalid as e:
64 errors = e.children
65 errors = e.children
65
66
66 def url_generator(**kw):
67 def url_generator(**kw):
67 q = urllib.quote(safe_str(search_query))
68 q = urllib.quote(safe_str(search_query))
68 return update_params(
69 return update_params(
69 "?q=%s&type=%s" % (q, safe_str(search_type)), **kw)
70 "?q=%s&type=%s" % (q, safe_str(search_type)), **kw)
70
71
71 search_query = search_params.get('search_query')
72 search_query = search_params.get('search_query')
72 search_type = search_params.get('search_type')
73 search_type = search_params.get('search_type')
73 search_sort = search_params.get('search_sort')
74 search_sort = search_params.get('search_sort')
74 if search_params.get('search_query'):
75 if search_params.get('search_query'):
75 page_limit = search_params['page_limit']
76 page_limit = search_params['page_limit']
76 requested_page = search_params['requested_page']
77 requested_page = search_params['requested_page']
77
78
78
79 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
79 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
80 ip_addr=self.ip_addr)
80 ip_addr=self.ip_addr)
81
81
82 try:
82 try:
83 search_result = searcher.search(
83 search_result = searcher.search(
84 search_query, search_type, c.perm_user, repo_name,
84 search_query, search_type, c.perm_user, repo_name,
85 requested_page, page_limit, search_sort)
85 requested_page, page_limit, search_sort)
86
86
87 formatted_results = Page(
87 formatted_results = Page(
88 search_result['results'], page=requested_page,
88 search_result['results'], page=requested_page,
89 item_count=search_result['count'],
89 item_count=search_result['count'],
90 items_per_page=page_limit, url=url_generator)
90 items_per_page=page_limit, url=url_generator)
91 finally:
91 finally:
92 searcher.cleanup()
92 searcher.cleanup()
93
93
94 if not search_result['error']:
94 if not search_result['error']:
95 execution_time = '%s results (%.3f seconds)' % (
95 execution_time = '%s results (%.3f seconds)' % (
96 search_result['count'],
96 search_result['count'],
97 search_result['runtime'])
97 search_result['runtime'])
98 elif not errors:
98 elif not errors:
99 node = schema['search_query']
99 node = schema['search_query']
100 errors = [
100 errors = [
101 validation_schema.Invalid(node, search_result['error'])]
101 validation_schema.Invalid(node, search_result['error'])]
102
102
103 c.sort = search_sort
103 c.sort = search_sort
104 c.url_generator = url_generator
104 c.url_generator = url_generator
105 c.errors = errors
105 c.errors = errors
106 c.formatted_results = formatted_results
106 c.formatted_results = formatted_results
107 c.runtime = execution_time
107 c.runtime = execution_time
108 c.cur_query = search_query
108 c.cur_query = search_query
109 c.search_type = search_type
109 c.search_type = search_type
110 # Return a rendered template
110 # Return a rendered template
111 return render('/search/search.html')
111 return render('/search/search.html')
@@ -1,580 +1,563 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 this is forms validation classes
22 this is forms validation classes
23 http://formencode.org/module-formencode.validators.html
23 http://formencode.org/module-formencode.validators.html
24 for list off all availible validators
24 for list off all availible validators
25
25
26 we can create our own validators
26 we can create our own validators
27
27
28 The table below outlines the options which can be used in a schema in addition to the validators themselves
28 The table below outlines the options which can be used in a schema in addition to the validators themselves
29 pre_validators [] These validators will be applied before the schema
29 pre_validators [] These validators will be applied before the schema
30 chained_validators [] These validators will be applied after the schema
30 chained_validators [] These validators will be applied after the schema
31 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
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 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
32 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
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.
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 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
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 <name> = formencode.validators.<name of validator>
37 <name> = formencode.validators.<name of validator>
38 <name> must equal form name
38 <name> must equal form name
39 list=[1,2,3,4,5]
39 list=[1,2,3,4,5]
40 for SELECT use formencode.All(OneOf(list), Int())
40 for SELECT use formencode.All(OneOf(list), Int())
41
41
42 """
42 """
43
43
44 import deform
44 import deform
45 import logging
45 import logging
46 import formencode
46 import formencode
47
47
48 from pkg_resources import resource_filename
48 from pkg_resources import resource_filename
49 from formencode import All, Pipe
49 from formencode import All, Pipe
50
50
51 from pylons.i18n.translation import _
51 from pylons.i18n.translation import _
52
52
53 from rhodecode import BACKENDS
53 from rhodecode import BACKENDS
54 from rhodecode.lib import helpers
54 from rhodecode.lib import helpers
55 from rhodecode.model import validators as v
55 from rhodecode.model import validators as v
56
56
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59
59
60 deform_templates = resource_filename('deform', 'templates')
60 deform_templates = resource_filename('deform', 'templates')
61 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
61 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
62 search_path = (rhodecode_templates, deform_templates)
62 search_path = (rhodecode_templates, deform_templates)
63
63
64
64
65 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
65 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
66 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
66 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
67 def __call__(self, template_name, **kw):
67 def __call__(self, template_name, **kw):
68 kw['h'] = helpers
68 kw['h'] = helpers
69 return self.load(template_name)(**kw)
69 return self.load(template_name)(**kw)
70
70
71
71
72 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
72 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
73 deform.Form.set_default_renderer(form_renderer)
73 deform.Form.set_default_renderer(form_renderer)
74
74
75
75
76 def LoginForm():
76 def LoginForm():
77 class _LoginForm(formencode.Schema):
77 class _LoginForm(formencode.Schema):
78 allow_extra_fields = True
78 allow_extra_fields = True
79 filter_extra_fields = True
79 filter_extra_fields = True
80 username = v.UnicodeString(
80 username = v.UnicodeString(
81 strip=True,
81 strip=True,
82 min=1,
82 min=1,
83 not_empty=True,
83 not_empty=True,
84 messages={
84 messages={
85 'empty': _(u'Please enter a login'),
85 'empty': _(u'Please enter a login'),
86 'tooShort': _(u'Enter a value %(min)i characters long or more')
86 'tooShort': _(u'Enter a value %(min)i characters long or more')
87 }
87 }
88 )
88 )
89
89
90 password = v.UnicodeString(
90 password = v.UnicodeString(
91 strip=False,
91 strip=False,
92 min=3,
92 min=3,
93 not_empty=True,
93 not_empty=True,
94 messages={
94 messages={
95 'empty': _(u'Please enter a password'),
95 'empty': _(u'Please enter a password'),
96 'tooShort': _(u'Enter %(min)i characters or more')}
96 'tooShort': _(u'Enter %(min)i characters or more')}
97 )
97 )
98
98
99 remember = v.StringBoolean(if_missing=False)
99 remember = v.StringBoolean(if_missing=False)
100
100
101 chained_validators = [v.ValidAuth()]
101 chained_validators = [v.ValidAuth()]
102 return _LoginForm
102 return _LoginForm
103
103
104
104
105 def PasswordChangeForm(username):
105 def PasswordChangeForm(username):
106 class _PasswordChangeForm(formencode.Schema):
106 class _PasswordChangeForm(formencode.Schema):
107 allow_extra_fields = True
107 allow_extra_fields = True
108 filter_extra_fields = True
108 filter_extra_fields = True
109
109
110 current_password = v.ValidOldPassword(username)(not_empty=True)
110 current_password = v.ValidOldPassword(username)(not_empty=True)
111 new_password = All(v.ValidPassword(), v.UnicodeString(strip=False, min=6))
111 new_password = All(v.ValidPassword(), v.UnicodeString(strip=False, min=6))
112 new_password_confirmation = All(v.ValidPassword(), v.UnicodeString(strip=False, min=6))
112 new_password_confirmation = All(v.ValidPassword(), v.UnicodeString(strip=False, min=6))
113
113
114 chained_validators = [v.ValidPasswordsMatch('new_password',
114 chained_validators = [v.ValidPasswordsMatch('new_password',
115 'new_password_confirmation')]
115 'new_password_confirmation')]
116 return _PasswordChangeForm
116 return _PasswordChangeForm
117
117
118
118
119 def UserForm(edit=False, available_languages=[], old_data={}):
119 def UserForm(edit=False, available_languages=[], old_data={}):
120 class _UserForm(formencode.Schema):
120 class _UserForm(formencode.Schema):
121 allow_extra_fields = True
121 allow_extra_fields = True
122 filter_extra_fields = True
122 filter_extra_fields = True
123 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
123 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
124 v.ValidUsername(edit, old_data))
124 v.ValidUsername(edit, old_data))
125 if edit:
125 if edit:
126 new_password = All(
126 new_password = All(
127 v.ValidPassword(),
127 v.ValidPassword(),
128 v.UnicodeString(strip=False, min=6, not_empty=False)
128 v.UnicodeString(strip=False, min=6, not_empty=False)
129 )
129 )
130 password_confirmation = All(
130 password_confirmation = All(
131 v.ValidPassword(),
131 v.ValidPassword(),
132 v.UnicodeString(strip=False, min=6, not_empty=False),
132 v.UnicodeString(strip=False, min=6, not_empty=False),
133 )
133 )
134 admin = v.StringBoolean(if_missing=False)
134 admin = v.StringBoolean(if_missing=False)
135 else:
135 else:
136 password = All(
136 password = All(
137 v.ValidPassword(),
137 v.ValidPassword(),
138 v.UnicodeString(strip=False, min=6, not_empty=True)
138 v.UnicodeString(strip=False, min=6, not_empty=True)
139 )
139 )
140 password_confirmation = All(
140 password_confirmation = All(
141 v.ValidPassword(),
141 v.ValidPassword(),
142 v.UnicodeString(strip=False, min=6, not_empty=False)
142 v.UnicodeString(strip=False, min=6, not_empty=False)
143 )
143 )
144
144
145 password_change = v.StringBoolean(if_missing=False)
145 password_change = v.StringBoolean(if_missing=False)
146 create_repo_group = v.StringBoolean(if_missing=False)
146 create_repo_group = v.StringBoolean(if_missing=False)
147
147
148 active = v.StringBoolean(if_missing=False)
148 active = v.StringBoolean(if_missing=False)
149 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
149 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
150 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
150 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
151 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
151 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
152 extern_name = v.UnicodeString(strip=True)
152 extern_name = v.UnicodeString(strip=True)
153 extern_type = v.UnicodeString(strip=True)
153 extern_type = v.UnicodeString(strip=True)
154 language = v.OneOf(available_languages, hideList=False,
154 language = v.OneOf(available_languages, hideList=False,
155 testValueList=True, if_missing=None)
155 testValueList=True, if_missing=None)
156 chained_validators = [v.ValidPasswordsMatch()]
156 chained_validators = [v.ValidPasswordsMatch()]
157 return _UserForm
157 return _UserForm
158
158
159
159
160 def UserGroupForm(edit=False, old_data=None, available_members=None,
160 def UserGroupForm(edit=False, old_data=None, available_members=None,
161 allow_disabled=False):
161 allow_disabled=False):
162 old_data = old_data or {}
162 old_data = old_data or {}
163 available_members = available_members or []
163 available_members = available_members or []
164
164
165 class _UserGroupForm(formencode.Schema):
165 class _UserGroupForm(formencode.Schema):
166 allow_extra_fields = True
166 allow_extra_fields = True
167 filter_extra_fields = True
167 filter_extra_fields = True
168
168
169 users_group_name = All(
169 users_group_name = All(
170 v.UnicodeString(strip=True, min=1, not_empty=True),
170 v.UnicodeString(strip=True, min=1, not_empty=True),
171 v.ValidUserGroup(edit, old_data)
171 v.ValidUserGroup(edit, old_data)
172 )
172 )
173 user_group_description = v.UnicodeString(strip=True, min=1,
173 user_group_description = v.UnicodeString(strip=True, min=1,
174 not_empty=False)
174 not_empty=False)
175
175
176 users_group_active = v.StringBoolean(if_missing=False)
176 users_group_active = v.StringBoolean(if_missing=False)
177
177
178 if edit:
178 if edit:
179 users_group_members = v.OneOf(
179 users_group_members = v.OneOf(
180 available_members, hideList=False, testValueList=True,
180 available_members, hideList=False, testValueList=True,
181 if_missing=None, not_empty=False
181 if_missing=None, not_empty=False
182 )
182 )
183 # this is user group owner
183 # this is user group owner
184 user = All(
184 user = All(
185 v.UnicodeString(not_empty=True),
185 v.UnicodeString(not_empty=True),
186 v.ValidRepoUser(allow_disabled))
186 v.ValidRepoUser(allow_disabled))
187 return _UserGroupForm
187 return _UserGroupForm
188
188
189
189
190 def RepoGroupForm(edit=False, old_data=None, available_groups=None,
190 def RepoGroupForm(edit=False, old_data=None, available_groups=None,
191 can_create_in_root=False, allow_disabled=False):
191 can_create_in_root=False, allow_disabled=False):
192 old_data = old_data or {}
192 old_data = old_data or {}
193 available_groups = available_groups or []
193 available_groups = available_groups or []
194
194
195 class _RepoGroupForm(formencode.Schema):
195 class _RepoGroupForm(formencode.Schema):
196 allow_extra_fields = True
196 allow_extra_fields = True
197 filter_extra_fields = False
197 filter_extra_fields = False
198
198
199 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
199 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
200 v.SlugifyName(),)
200 v.SlugifyName(),)
201 group_description = v.UnicodeString(strip=True, min=1,
201 group_description = v.UnicodeString(strip=True, min=1,
202 not_empty=False)
202 not_empty=False)
203 group_copy_permissions = v.StringBoolean(if_missing=False)
203 group_copy_permissions = v.StringBoolean(if_missing=False)
204
204
205 group_parent_id = v.OneOf(available_groups, hideList=False,
205 group_parent_id = v.OneOf(available_groups, hideList=False,
206 testValueList=True, not_empty=True)
206 testValueList=True, not_empty=True)
207 enable_locking = v.StringBoolean(if_missing=False)
207 enable_locking = v.StringBoolean(if_missing=False)
208 chained_validators = [
208 chained_validators = [
209 v.ValidRepoGroup(edit, old_data, can_create_in_root)]
209 v.ValidRepoGroup(edit, old_data, can_create_in_root)]
210
210
211 if edit:
211 if edit:
212 # this is repo group owner
212 # this is repo group owner
213 user = All(
213 user = All(
214 v.UnicodeString(not_empty=True),
214 v.UnicodeString(not_empty=True),
215 v.ValidRepoUser(allow_disabled))
215 v.ValidRepoUser(allow_disabled))
216
216
217 return _RepoGroupForm
217 return _RepoGroupForm
218
218
219
219
220 def RegisterForm(edit=False, old_data={}):
220 def RegisterForm(edit=False, old_data={}):
221 class _RegisterForm(formencode.Schema):
221 class _RegisterForm(formencode.Schema):
222 allow_extra_fields = True
222 allow_extra_fields = True
223 filter_extra_fields = True
223 filter_extra_fields = True
224 username = All(
224 username = All(
225 v.ValidUsername(edit, old_data),
225 v.ValidUsername(edit, old_data),
226 v.UnicodeString(strip=True, min=1, not_empty=True)
226 v.UnicodeString(strip=True, min=1, not_empty=True)
227 )
227 )
228 password = All(
228 password = All(
229 v.ValidPassword(),
229 v.ValidPassword(),
230 v.UnicodeString(strip=False, min=6, not_empty=True)
230 v.UnicodeString(strip=False, min=6, not_empty=True)
231 )
231 )
232 password_confirmation = All(
232 password_confirmation = All(
233 v.ValidPassword(),
233 v.ValidPassword(),
234 v.UnicodeString(strip=False, min=6, not_empty=True)
234 v.UnicodeString(strip=False, min=6, not_empty=True)
235 )
235 )
236 active = v.StringBoolean(if_missing=False)
236 active = v.StringBoolean(if_missing=False)
237 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
237 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
238 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
238 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
239 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
239 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
240
240
241 chained_validators = [v.ValidPasswordsMatch()]
241 chained_validators = [v.ValidPasswordsMatch()]
242
242
243 return _RegisterForm
243 return _RegisterForm
244
244
245
245
246 def PasswordResetForm():
246 def PasswordResetForm():
247 class _PasswordResetForm(formencode.Schema):
247 class _PasswordResetForm(formencode.Schema):
248 allow_extra_fields = True
248 allow_extra_fields = True
249 filter_extra_fields = True
249 filter_extra_fields = True
250 email = All(v.ValidSystemEmail(), v.Email(not_empty=True))
250 email = All(v.ValidSystemEmail(), v.Email(not_empty=True))
251 return _PasswordResetForm
251 return _PasswordResetForm
252
252
253
253
254 def RepoForm(edit=False, old_data=None, repo_groups=None, landing_revs=None,
254 def RepoForm(edit=False, old_data=None, repo_groups=None, landing_revs=None,
255 allow_disabled=False):
255 allow_disabled=False):
256 old_data = old_data or {}
256 old_data = old_data or {}
257 repo_groups = repo_groups or []
257 repo_groups = repo_groups or []
258 landing_revs = landing_revs or []
258 landing_revs = landing_revs or []
259 supported_backends = BACKENDS.keys()
259 supported_backends = BACKENDS.keys()
260
260
261 class _RepoForm(formencode.Schema):
261 class _RepoForm(formencode.Schema):
262 allow_extra_fields = True
262 allow_extra_fields = True
263 filter_extra_fields = False
263 filter_extra_fields = False
264 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
264 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
265 v.SlugifyName())
265 v.SlugifyName())
266 repo_group = All(v.CanWriteGroup(old_data),
266 repo_group = All(v.CanWriteGroup(old_data),
267 v.OneOf(repo_groups, hideList=True))
267 v.OneOf(repo_groups, hideList=True))
268 repo_type = v.OneOf(supported_backends, required=False,
268 repo_type = v.OneOf(supported_backends, required=False,
269 if_missing=old_data.get('repo_type'))
269 if_missing=old_data.get('repo_type'))
270 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
270 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
271 repo_private = v.StringBoolean(if_missing=False)
271 repo_private = v.StringBoolean(if_missing=False)
272 repo_landing_rev = v.OneOf(landing_revs, hideList=True)
272 repo_landing_rev = v.OneOf(landing_revs, hideList=True)
273 repo_copy_permissions = v.StringBoolean(if_missing=False)
273 repo_copy_permissions = v.StringBoolean(if_missing=False)
274 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
274 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
275
275
276 repo_enable_statistics = v.StringBoolean(if_missing=False)
276 repo_enable_statistics = v.StringBoolean(if_missing=False)
277 repo_enable_downloads = v.StringBoolean(if_missing=False)
277 repo_enable_downloads = v.StringBoolean(if_missing=False)
278 repo_enable_locking = v.StringBoolean(if_missing=False)
278 repo_enable_locking = v.StringBoolean(if_missing=False)
279
279
280 if edit:
280 if edit:
281 # this is repo owner
281 # this is repo owner
282 user = All(
282 user = All(
283 v.UnicodeString(not_empty=True),
283 v.UnicodeString(not_empty=True),
284 v.ValidRepoUser(allow_disabled))
284 v.ValidRepoUser(allow_disabled))
285 clone_uri_change = v.UnicodeString(
285 clone_uri_change = v.UnicodeString(
286 not_empty=False, if_missing=v.Missing)
286 not_empty=False, if_missing=v.Missing)
287
287
288 chained_validators = [v.ValidCloneUri(),
288 chained_validators = [v.ValidCloneUri(),
289 v.ValidRepoName(edit, old_data)]
289 v.ValidRepoName(edit, old_data)]
290 return _RepoForm
290 return _RepoForm
291
291
292
292
293 def RepoPermsForm():
293 def RepoPermsForm():
294 class _RepoPermsForm(formencode.Schema):
294 class _RepoPermsForm(formencode.Schema):
295 allow_extra_fields = True
295 allow_extra_fields = True
296 filter_extra_fields = False
296 filter_extra_fields = False
297 chained_validators = [v.ValidPerms(type_='repo')]
297 chained_validators = [v.ValidPerms(type_='repo')]
298 return _RepoPermsForm
298 return _RepoPermsForm
299
299
300
300
301 def RepoGroupPermsForm(valid_recursive_choices):
301 def RepoGroupPermsForm(valid_recursive_choices):
302 class _RepoGroupPermsForm(formencode.Schema):
302 class _RepoGroupPermsForm(formencode.Schema):
303 allow_extra_fields = True
303 allow_extra_fields = True
304 filter_extra_fields = False
304 filter_extra_fields = False
305 recursive = v.OneOf(valid_recursive_choices)
305 recursive = v.OneOf(valid_recursive_choices)
306 chained_validators = [v.ValidPerms(type_='repo_group')]
306 chained_validators = [v.ValidPerms(type_='repo_group')]
307 return _RepoGroupPermsForm
307 return _RepoGroupPermsForm
308
308
309
309
310 def UserGroupPermsForm():
310 def UserGroupPermsForm():
311 class _UserPermsForm(formencode.Schema):
311 class _UserPermsForm(formencode.Schema):
312 allow_extra_fields = True
312 allow_extra_fields = True
313 filter_extra_fields = False
313 filter_extra_fields = False
314 chained_validators = [v.ValidPerms(type_='user_group')]
314 chained_validators = [v.ValidPerms(type_='user_group')]
315 return _UserPermsForm
315 return _UserPermsForm
316
316
317
317
318 def RepoFieldForm():
318 def RepoFieldForm():
319 class _RepoFieldForm(formencode.Schema):
319 class _RepoFieldForm(formencode.Schema):
320 filter_extra_fields = True
320 filter_extra_fields = True
321 allow_extra_fields = True
321 allow_extra_fields = True
322
322
323 new_field_key = All(v.FieldKey(),
323 new_field_key = All(v.FieldKey(),
324 v.UnicodeString(strip=True, min=3, not_empty=True))
324 v.UnicodeString(strip=True, min=3, not_empty=True))
325 new_field_value = v.UnicodeString(not_empty=False, if_missing=u'')
325 new_field_value = v.UnicodeString(not_empty=False, if_missing=u'')
326 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
326 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
327 if_missing='str')
327 if_missing='str')
328 new_field_label = v.UnicodeString(not_empty=False)
328 new_field_label = v.UnicodeString(not_empty=False)
329 new_field_desc = v.UnicodeString(not_empty=False)
329 new_field_desc = v.UnicodeString(not_empty=False)
330
330
331 return _RepoFieldForm
331 return _RepoFieldForm
332
332
333
333
334 def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
334 def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
335 repo_groups=[], landing_revs=[]):
335 repo_groups=[], landing_revs=[]):
336 class _RepoForkForm(formencode.Schema):
336 class _RepoForkForm(formencode.Schema):
337 allow_extra_fields = True
337 allow_extra_fields = True
338 filter_extra_fields = False
338 filter_extra_fields = False
339 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
339 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
340 v.SlugifyName())
340 v.SlugifyName())
341 repo_group = All(v.CanWriteGroup(),
341 repo_group = All(v.CanWriteGroup(),
342 v.OneOf(repo_groups, hideList=True))
342 v.OneOf(repo_groups, hideList=True))
343 repo_type = All(v.ValidForkType(old_data), v.OneOf(supported_backends))
343 repo_type = All(v.ValidForkType(old_data), v.OneOf(supported_backends))
344 description = v.UnicodeString(strip=True, min=1, not_empty=True)
344 description = v.UnicodeString(strip=True, min=1, not_empty=True)
345 private = v.StringBoolean(if_missing=False)
345 private = v.StringBoolean(if_missing=False)
346 copy_permissions = v.StringBoolean(if_missing=False)
346 copy_permissions = v.StringBoolean(if_missing=False)
347 fork_parent_id = v.UnicodeString()
347 fork_parent_id = v.UnicodeString()
348 chained_validators = [v.ValidForkName(edit, old_data)]
348 chained_validators = [v.ValidForkName(edit, old_data)]
349 landing_rev = v.OneOf(landing_revs, hideList=True)
349 landing_rev = v.OneOf(landing_revs, hideList=True)
350
350
351 return _RepoForkForm
351 return _RepoForkForm
352
352
353
353
354 def ApplicationSettingsForm():
354 def ApplicationSettingsForm():
355 class _ApplicationSettingsForm(formencode.Schema):
355 class _ApplicationSettingsForm(formencode.Schema):
356 allow_extra_fields = True
356 allow_extra_fields = True
357 filter_extra_fields = False
357 filter_extra_fields = False
358 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
358 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
359 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
359 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
360 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
360 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
361 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
361 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
362 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
362 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
363 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
363 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
364
364
365 return _ApplicationSettingsForm
365 return _ApplicationSettingsForm
366
366
367
367
368 def ApplicationVisualisationForm():
368 def ApplicationVisualisationForm():
369 class _ApplicationVisualisationForm(formencode.Schema):
369 class _ApplicationVisualisationForm(formencode.Schema):
370 allow_extra_fields = True
370 allow_extra_fields = True
371 filter_extra_fields = False
371 filter_extra_fields = False
372 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
372 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
373 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
373 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
374 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
374 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
375
375
376 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
376 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
377 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
377 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
378 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
378 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
379 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
379 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
380 rhodecode_show_version = v.StringBoolean(if_missing=False)
380 rhodecode_show_version = v.StringBoolean(if_missing=False)
381 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
381 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
382 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
382 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
383 rhodecode_gravatar_url = v.UnicodeString(min=3)
383 rhodecode_gravatar_url = v.UnicodeString(min=3)
384 rhodecode_clone_uri_tmpl = v.UnicodeString(min=3)
384 rhodecode_clone_uri_tmpl = v.UnicodeString(min=3)
385 rhodecode_support_url = v.UnicodeString()
385 rhodecode_support_url = v.UnicodeString()
386 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
386 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
387 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
387 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
388
388
389 return _ApplicationVisualisationForm
389 return _ApplicationVisualisationForm
390
390
391
391
392 class _BaseVcsSettingsForm(formencode.Schema):
392 class _BaseVcsSettingsForm(formencode.Schema):
393 allow_extra_fields = True
393 allow_extra_fields = True
394 filter_extra_fields = False
394 filter_extra_fields = False
395 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
395 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
396 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
396 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
397 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
397 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
398
398
399 extensions_largefiles = v.StringBoolean(if_missing=False)
399 extensions_largefiles = v.StringBoolean(if_missing=False)
400 phases_publish = v.StringBoolean(if_missing=False)
400 phases_publish = v.StringBoolean(if_missing=False)
401
401
402 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
402 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
403 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
403 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
404 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
404 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
405
405
406
406
407 def ApplicationUiSettingsForm():
407 def ApplicationUiSettingsForm():
408 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
408 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
409 web_push_ssl = v.StringBoolean(if_missing=False)
409 web_push_ssl = v.StringBoolean(if_missing=False)
410 paths_root_path = All(
410 paths_root_path = All(
411 v.ValidPath(),
411 v.ValidPath(),
412 v.UnicodeString(strip=True, min=1, not_empty=True)
412 v.UnicodeString(strip=True, min=1, not_empty=True)
413 )
413 )
414 extensions_hgsubversion = v.StringBoolean(if_missing=False)
414 extensions_hgsubversion = v.StringBoolean(if_missing=False)
415 extensions_hggit = v.StringBoolean(if_missing=False)
415 extensions_hggit = v.StringBoolean(if_missing=False)
416 new_svn_branch = v.ValidSvnPattern(section='vcs_svn_branch')
416 new_svn_branch = v.ValidSvnPattern(section='vcs_svn_branch')
417 new_svn_tag = v.ValidSvnPattern(section='vcs_svn_tag')
417 new_svn_tag = v.ValidSvnPattern(section='vcs_svn_tag')
418
418
419 return _ApplicationUiSettingsForm
419 return _ApplicationUiSettingsForm
420
420
421
421
422 def RepoVcsSettingsForm(repo_name):
422 def RepoVcsSettingsForm(repo_name):
423 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
423 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
424 inherit_global_settings = v.StringBoolean(if_missing=False)
424 inherit_global_settings = v.StringBoolean(if_missing=False)
425 new_svn_branch = v.ValidSvnPattern(
425 new_svn_branch = v.ValidSvnPattern(
426 section='vcs_svn_branch', repo_name=repo_name)
426 section='vcs_svn_branch', repo_name=repo_name)
427 new_svn_tag = v.ValidSvnPattern(
427 new_svn_tag = v.ValidSvnPattern(
428 section='vcs_svn_tag', repo_name=repo_name)
428 section='vcs_svn_tag', repo_name=repo_name)
429
429
430 return _RepoVcsSettingsForm
430 return _RepoVcsSettingsForm
431
431
432
432
433 def LabsSettingsForm():
433 def LabsSettingsForm():
434 class _LabSettingsForm(formencode.Schema):
434 class _LabSettingsForm(formencode.Schema):
435 allow_extra_fields = True
435 allow_extra_fields = True
436 filter_extra_fields = False
436 filter_extra_fields = False
437
437
438 rhodecode_proxy_subversion_http_requests = v.StringBoolean(
438 rhodecode_proxy_subversion_http_requests = v.StringBoolean(
439 if_missing=False)
439 if_missing=False)
440 rhodecode_subversion_http_server_url = v.UnicodeString(
440 rhodecode_subversion_http_server_url = v.UnicodeString(
441 strip=True, if_missing=None)
441 strip=True, if_missing=None)
442
442
443 return _LabSettingsForm
443 return _LabSettingsForm
444
444
445
445
446 def ApplicationPermissionsForm(register_choices, extern_activate_choices):
446 def ApplicationPermissionsForm(register_choices, extern_activate_choices):
447 class _DefaultPermissionsForm(formencode.Schema):
447 class _DefaultPermissionsForm(formencode.Schema):
448 allow_extra_fields = True
448 allow_extra_fields = True
449 filter_extra_fields = True
449 filter_extra_fields = True
450
450
451 anonymous = v.StringBoolean(if_missing=False)
451 anonymous = v.StringBoolean(if_missing=False)
452 default_register = v.OneOf(register_choices)
452 default_register = v.OneOf(register_choices)
453 default_register_message = v.UnicodeString()
453 default_register_message = v.UnicodeString()
454 default_extern_activate = v.OneOf(extern_activate_choices)
454 default_extern_activate = v.OneOf(extern_activate_choices)
455
455
456 return _DefaultPermissionsForm
456 return _DefaultPermissionsForm
457
457
458
458
459 def ObjectPermissionsForm(repo_perms_choices, group_perms_choices,
459 def ObjectPermissionsForm(repo_perms_choices, group_perms_choices,
460 user_group_perms_choices):
460 user_group_perms_choices):
461 class _ObjectPermissionsForm(formencode.Schema):
461 class _ObjectPermissionsForm(formencode.Schema):
462 allow_extra_fields = True
462 allow_extra_fields = True
463 filter_extra_fields = True
463 filter_extra_fields = True
464 overwrite_default_repo = v.StringBoolean(if_missing=False)
464 overwrite_default_repo = v.StringBoolean(if_missing=False)
465 overwrite_default_group = v.StringBoolean(if_missing=False)
465 overwrite_default_group = v.StringBoolean(if_missing=False)
466 overwrite_default_user_group = v.StringBoolean(if_missing=False)
466 overwrite_default_user_group = v.StringBoolean(if_missing=False)
467 default_repo_perm = v.OneOf(repo_perms_choices)
467 default_repo_perm = v.OneOf(repo_perms_choices)
468 default_group_perm = v.OneOf(group_perms_choices)
468 default_group_perm = v.OneOf(group_perms_choices)
469 default_user_group_perm = v.OneOf(user_group_perms_choices)
469 default_user_group_perm = v.OneOf(user_group_perms_choices)
470
470
471 return _ObjectPermissionsForm
471 return _ObjectPermissionsForm
472
472
473
473
474 def UserPermissionsForm(create_choices, create_on_write_choices,
474 def UserPermissionsForm(create_choices, create_on_write_choices,
475 repo_group_create_choices, user_group_create_choices,
475 repo_group_create_choices, user_group_create_choices,
476 fork_choices, inherit_default_permissions_choices):
476 fork_choices, inherit_default_permissions_choices):
477 class _DefaultPermissionsForm(formencode.Schema):
477 class _DefaultPermissionsForm(formencode.Schema):
478 allow_extra_fields = True
478 allow_extra_fields = True
479 filter_extra_fields = True
479 filter_extra_fields = True
480
480
481 anonymous = v.StringBoolean(if_missing=False)
481 anonymous = v.StringBoolean(if_missing=False)
482
482
483 default_repo_create = v.OneOf(create_choices)
483 default_repo_create = v.OneOf(create_choices)
484 default_repo_create_on_write = v.OneOf(create_on_write_choices)
484 default_repo_create_on_write = v.OneOf(create_on_write_choices)
485 default_user_group_create = v.OneOf(user_group_create_choices)
485 default_user_group_create = v.OneOf(user_group_create_choices)
486 default_repo_group_create = v.OneOf(repo_group_create_choices)
486 default_repo_group_create = v.OneOf(repo_group_create_choices)
487 default_fork_create = v.OneOf(fork_choices)
487 default_fork_create = v.OneOf(fork_choices)
488 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
488 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
489
489
490 return _DefaultPermissionsForm
490 return _DefaultPermissionsForm
491
491
492
492
493 def UserIndividualPermissionsForm():
493 def UserIndividualPermissionsForm():
494 class _DefaultPermissionsForm(formencode.Schema):
494 class _DefaultPermissionsForm(formencode.Schema):
495 allow_extra_fields = True
495 allow_extra_fields = True
496 filter_extra_fields = True
496 filter_extra_fields = True
497
497
498 inherit_default_permissions = v.StringBoolean(if_missing=False)
498 inherit_default_permissions = v.StringBoolean(if_missing=False)
499
499
500 return _DefaultPermissionsForm
500 return _DefaultPermissionsForm
501
501
502
502
503 def DefaultsForm(edit=False, old_data={}, supported_backends=BACKENDS.keys()):
503 def DefaultsForm(edit=False, old_data={}, supported_backends=BACKENDS.keys()):
504 class _DefaultsForm(formencode.Schema):
504 class _DefaultsForm(formencode.Schema):
505 allow_extra_fields = True
505 allow_extra_fields = True
506 filter_extra_fields = True
506 filter_extra_fields = True
507 default_repo_type = v.OneOf(supported_backends)
507 default_repo_type = v.OneOf(supported_backends)
508 default_repo_private = v.StringBoolean(if_missing=False)
508 default_repo_private = v.StringBoolean(if_missing=False)
509 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
509 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
510 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
510 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
511 default_repo_enable_locking = v.StringBoolean(if_missing=False)
511 default_repo_enable_locking = v.StringBoolean(if_missing=False)
512
512
513 return _DefaultsForm
513 return _DefaultsForm
514
514
515
515
516 def AuthSettingsForm():
516 def AuthSettingsForm():
517 class _AuthSettingsForm(formencode.Schema):
517 class _AuthSettingsForm(formencode.Schema):
518 allow_extra_fields = True
518 allow_extra_fields = True
519 filter_extra_fields = True
519 filter_extra_fields = True
520 auth_plugins = All(v.ValidAuthPlugins(),
520 auth_plugins = All(v.ValidAuthPlugins(),
521 v.UniqueListFromString()(not_empty=True))
521 v.UniqueListFromString()(not_empty=True))
522
522
523 return _AuthSettingsForm
523 return _AuthSettingsForm
524
524
525
525
526 def UserExtraEmailForm():
526 def UserExtraEmailForm():
527 class _UserExtraEmailForm(formencode.Schema):
527 class _UserExtraEmailForm(formencode.Schema):
528 email = All(v.UniqSystemEmail(), v.Email(not_empty=True))
528 email = All(v.UniqSystemEmail(), v.Email(not_empty=True))
529 return _UserExtraEmailForm
529 return _UserExtraEmailForm
530
530
531
531
532 def UserExtraIpForm():
532 def UserExtraIpForm():
533 class _UserExtraIpForm(formencode.Schema):
533 class _UserExtraIpForm(formencode.Schema):
534 ip = v.ValidIp()(not_empty=True)
534 ip = v.ValidIp()(not_empty=True)
535 return _UserExtraIpForm
535 return _UserExtraIpForm
536
536
537
537
538 def PullRequestForm(repo_id):
538 def PullRequestForm(repo_id):
539 class _PullRequestForm(formencode.Schema):
539 class _PullRequestForm(formencode.Schema):
540 allow_extra_fields = True
540 allow_extra_fields = True
541 filter_extra_fields = True
541 filter_extra_fields = True
542
542
543 user = v.UnicodeString(strip=True, required=True)
543 user = v.UnicodeString(strip=True, required=True)
544 source_repo = v.UnicodeString(strip=True, required=True)
544 source_repo = v.UnicodeString(strip=True, required=True)
545 source_ref = v.UnicodeString(strip=True, required=True)
545 source_ref = v.UnicodeString(strip=True, required=True)
546 target_repo = v.UnicodeString(strip=True, required=True)
546 target_repo = v.UnicodeString(strip=True, required=True)
547 target_ref = v.UnicodeString(strip=True, required=True)
547 target_ref = v.UnicodeString(strip=True, required=True)
548 revisions = All(#v.NotReviewedRevisions(repo_id)(),
548 revisions = All(#v.NotReviewedRevisions(repo_id)(),
549 v.UniqueList()(not_empty=True))
549 v.UniqueList()(not_empty=True))
550 review_members = v.UniqueList(convert=int)(not_empty=True)
550 review_members = v.UniqueList(convert=int)(not_empty=True)
551
551
552 pullrequest_title = v.UnicodeString(strip=True, required=True)
552 pullrequest_title = v.UnicodeString(strip=True, required=True)
553 pullrequest_desc = v.UnicodeString(strip=True, required=False)
553 pullrequest_desc = v.UnicodeString(strip=True, required=False)
554
554
555 return _PullRequestForm
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 def IssueTrackerPatternsForm():
558 def IssueTrackerPatternsForm():
576 class _IssueTrackerPatternsForm(formencode.Schema):
559 class _IssueTrackerPatternsForm(formencode.Schema):
577 allow_extra_fields = True
560 allow_extra_fields = True
578 filter_extra_fields = False
561 filter_extra_fields = False
579 chained_validators = [v.ValidPattern()]
562 chained_validators = [v.ValidPattern()]
580 return _IssueTrackerPatternsForm
563 return _IssueTrackerPatternsForm
@@ -1,252 +1,236 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2013-2016 RhodeCode GmbH
3 # Copyright (C) 2013-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 gist model for RhodeCode
22 gist model for RhodeCode
23 """
23 """
24
24
25 import os
25 import os
26 import time
26 import time
27 import logging
27 import logging
28 import traceback
28 import traceback
29 import shutil
29 import shutil
30
30
31 from rhodecode.lib.utils2 import (
31 from rhodecode.lib.utils2 import (
32 safe_unicode, unique_id, safe_int, time_to_datetime, AttributeDict)
32 safe_unicode, unique_id, safe_int, time_to_datetime, AttributeDict)
33 from rhodecode.lib.ext_json import json
33 from rhodecode.lib.ext_json import json
34 from rhodecode.model import BaseModel
34 from rhodecode.model import BaseModel
35 from rhodecode.model.db import Gist
35 from rhodecode.model.db import Gist
36 from rhodecode.model.repo import RepoModel
36 from rhodecode.model.repo import RepoModel
37 from rhodecode.model.scm import ScmModel
37 from rhodecode.model.scm import ScmModel
38
38
39 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
40
40
41 GIST_STORE_LOC = '.rc_gist_store'
41 GIST_STORE_LOC = '.rc_gist_store'
42 GIST_METADATA_FILE = '.rc_gist_metadata'
42 GIST_METADATA_FILE = '.rc_gist_metadata'
43
43
44
44
45 class GistModel(BaseModel):
45 class GistModel(BaseModel):
46 cls = Gist
46 cls = Gist
47
47
48 def _get_gist(self, gist):
48 def _get_gist(self, gist):
49 """
49 """
50 Helper method to get gist by ID, or gist_access_id as a fallback
50 Helper method to get gist by ID, or gist_access_id as a fallback
51
51
52 :param gist: GistID, gist_access_id, or Gist instance
52 :param gist: GistID, gist_access_id, or Gist instance
53 """
53 """
54 return self._get_instance(Gist, gist, callback=Gist.get_by_access_id)
54 return self._get_instance(Gist, gist, callback=Gist.get_by_access_id)
55
55
56 def __delete_gist(self, gist):
56 def __delete_gist(self, gist):
57 """
57 """
58 removes gist from filesystem
58 removes gist from filesystem
59
59
60 :param gist: gist object
60 :param gist: gist object
61 """
61 """
62 root_path = RepoModel().repos_path
62 root_path = RepoModel().repos_path
63 rm_path = os.path.join(root_path, GIST_STORE_LOC, gist.gist_access_id)
63 rm_path = os.path.join(root_path, GIST_STORE_LOC, gist.gist_access_id)
64 log.info("Removing %s", rm_path)
64 log.info("Removing %s", rm_path)
65 shutil.rmtree(rm_path)
65 shutil.rmtree(rm_path)
66
66
67 def _store_metadata(self, repo, gist_id, gist_access_id, user_id, username,
67 def _store_metadata(self, repo, gist_id, gist_access_id, user_id, username,
68 gist_type, gist_expires, gist_acl_level):
68 gist_type, gist_expires, gist_acl_level):
69 """
69 """
70 store metadata inside the gist repo, this can be later used for imports
70 store metadata inside the gist repo, this can be later used for imports
71 or gist identification. Currently we use this inside RhodeCode tools
71 or gist identification. Currently we use this inside RhodeCode tools
72 to do cleanup of gists that are in storage but not in database.
72 to do cleanup of gists that are in storage but not in database.
73 """
73 """
74 metadata = {
74 metadata = {
75 'metadata_version': '2',
75 'metadata_version': '2',
76 'gist_db_id': gist_id,
76 'gist_db_id': gist_id,
77 'gist_access_id': gist_access_id,
77 'gist_access_id': gist_access_id,
78 'gist_owner_id': user_id,
78 'gist_owner_id': user_id,
79 'gist_owner_username': username,
79 'gist_owner_username': username,
80 'gist_type': gist_type,
80 'gist_type': gist_type,
81 'gist_expires': gist_expires,
81 'gist_expires': gist_expires,
82 'gist_updated': time.time(),
82 'gist_updated': time.time(),
83 'gist_acl_level': gist_acl_level,
83 'gist_acl_level': gist_acl_level,
84 }
84 }
85 metadata_file = os.path.join(repo.path, '.hg', GIST_METADATA_FILE)
85 metadata_file = os.path.join(repo.path, '.hg', GIST_METADATA_FILE)
86 with open(metadata_file, 'wb') as f:
86 with open(metadata_file, 'wb') as f:
87 f.write(json.dumps(metadata))
87 f.write(json.dumps(metadata))
88
88
89 def get_gist(self, gist):
89 def get_gist(self, gist):
90 return self._get_gist(gist)
90 return self._get_gist(gist)
91
91
92 def get_gist_files(self, gist_access_id, revision=None):
92 def get_gist_files(self, gist_access_id, revision=None):
93 """
93 """
94 Get files for given gist
94 Get files for given gist
95
95
96 :param gist_access_id:
96 :param gist_access_id:
97 """
97 """
98 repo = Gist.get_by_access_id(gist_access_id)
98 repo = Gist.get_by_access_id(gist_access_id)
99 commit = repo.scm_instance().get_commit(commit_id=revision)
99 commit = repo.scm_instance().get_commit(commit_id=revision)
100 return commit, [n for n in commit.get_node('/')]
100 return commit, [n for n in commit.get_node('/')]
101
101
102 def create(self, description, owner, gist_mapping,
102 def create(self, description, owner, gist_mapping,
103 gist_type=Gist.GIST_PUBLIC, lifetime=-1, gist_id=None,
103 gist_type=Gist.GIST_PUBLIC, lifetime=-1, gist_id=None,
104 gist_acl_level=Gist.ACL_LEVEL_PRIVATE):
104 gist_acl_level=Gist.ACL_LEVEL_PRIVATE):
105 """
105 """
106 Create a gist
106 Create a gist
107
107
108 :param description: description of the gist
108 :param description: description of the gist
109 :param owner: user who created this gist
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 :param gist_type: type of gist private/public
111 :param gist_type: type of gist private/public
112 :param lifetime: in minutes, -1 == forever
112 :param lifetime: in minutes, -1 == forever
113 :param gist_acl_level: acl level for this gist
113 :param gist_acl_level: acl level for this gist
114 """
114 """
115 owner = self._get_user(owner)
115 owner = self._get_user(owner)
116 gist_id = safe_unicode(gist_id or unique_id(20))
116 gist_id = safe_unicode(gist_id or unique_id(20))
117 lifetime = safe_int(lifetime, -1)
117 lifetime = safe_int(lifetime, -1)
118 gist_expires = time.time() + (lifetime * 60) if lifetime != -1 else -1
118 gist_expires = time.time() + (lifetime * 60) if lifetime != -1 else -1
119 expiration = (time_to_datetime(gist_expires)
119 expiration = (time_to_datetime(gist_expires)
120 if gist_expires != -1 else 'forever')
120 if gist_expires != -1 else 'forever')
121 log.debug('set GIST expiration date to: %s', expiration)
121 log.debug('set GIST expiration date to: %s', expiration)
122 # create the Database version
122 # create the Database version
123 gist = Gist()
123 gist = Gist()
124 gist.gist_description = description
124 gist.gist_description = description
125 gist.gist_access_id = gist_id
125 gist.gist_access_id = gist_id
126 gist.gist_owner = owner.user_id
126 gist.gist_owner = owner.user_id
127 gist.gist_expires = gist_expires
127 gist.gist_expires = gist_expires
128 gist.gist_type = safe_unicode(gist_type)
128 gist.gist_type = safe_unicode(gist_type)
129 gist.acl_level = gist_acl_level
129 gist.acl_level = gist_acl_level
130 self.sa.add(gist)
130 self.sa.add(gist)
131 self.sa.flush()
131 self.sa.flush()
132 if gist_type == Gist.GIST_PUBLIC:
132 if gist_type == Gist.GIST_PUBLIC:
133 # use DB ID for easy to use GIST ID
133 # use DB ID for easy to use GIST ID
134 gist_id = safe_unicode(gist.gist_id)
134 gist_id = safe_unicode(gist.gist_id)
135 gist.gist_access_id = gist_id
135 gist.gist_access_id = gist_id
136 self.sa.add(gist)
136 self.sa.add(gist)
137
137
138 gist_repo_path = os.path.join(GIST_STORE_LOC, gist_id)
138 gist_repo_path = os.path.join(GIST_STORE_LOC, gist_id)
139 log.debug('Creating new %s GIST repo in %s', gist_type, gist_repo_path)
139 log.debug('Creating new %s GIST repo in %s', gist_type, gist_repo_path)
140 repo = RepoModel()._create_filesystem_repo(
140 repo = RepoModel()._create_filesystem_repo(
141 repo_name=gist_id, repo_type='hg', repo_group=GIST_STORE_LOC,
141 repo_name=gist_id, repo_type='hg', repo_group=GIST_STORE_LOC,
142 use_global_config=True)
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 # now create single multifile commit
144 # now create single multifile commit
160 message = 'added file'
145 message = 'added file'
161 message += 's: ' if len(processed_mapping) > 1 else ': '
146 message += 's: ' if len(gist_mapping) > 1 else ': '
162 message += ', '.join([x for x in processed_mapping])
147 message += ', '.join([x for x in gist_mapping])
163
148
164 # fake RhodeCode Repository object
149 # fake RhodeCode Repository object
165 fake_repo = AttributeDict({
150 fake_repo = AttributeDict({
166 'repo_name': gist_repo_path,
151 'repo_name': gist_repo_path,
167 'scm_instance': lambda *args, **kwargs: repo,
152 'scm_instance': lambda *args, **kwargs: repo,
168 })
153 })
169
154
170 ScmModel().create_nodes(
155 ScmModel().create_nodes(
171 user=owner.user_id, repo=fake_repo,
156 user=owner.user_id, repo=fake_repo,
172 message=message,
157 message=message,
173 nodes=processed_mapping,
158 nodes=gist_mapping,
174 trigger_push_hook=False
159 trigger_push_hook=False
175 )
160 )
176
161
177 self._store_metadata(repo, gist.gist_id, gist.gist_access_id,
162 self._store_metadata(repo, gist.gist_id, gist.gist_access_id,
178 owner.user_id, owner.username, gist.gist_type,
163 owner.user_id, owner.username, gist.gist_type,
179 gist.gist_expires, gist_acl_level)
164 gist.gist_expires, gist_acl_level)
180 return gist
165 return gist
181
166
182 def delete(self, gist, fs_remove=True):
167 def delete(self, gist, fs_remove=True):
183 gist = self._get_gist(gist)
168 gist = self._get_gist(gist)
184 try:
169 try:
185 self.sa.delete(gist)
170 self.sa.delete(gist)
186 if fs_remove:
171 if fs_remove:
187 self.__delete_gist(gist)
172 self.__delete_gist(gist)
188 else:
173 else:
189 log.debug('skipping removal from filesystem')
174 log.debug('skipping removal from filesystem')
190 except Exception:
175 except Exception:
191 log.error(traceback.format_exc())
176 log.error(traceback.format_exc())
192 raise
177 raise
193
178
194 def update(self, gist, description, owner, gist_mapping, gist_type,
179 def update(self, gist, description, owner, gist_mapping, gist_type,
195 lifetime, gist_acl_level):
180 lifetime, gist_acl_level):
196 gist = self._get_gist(gist)
181 gist = self._get_gist(gist)
197 gist_repo = gist.scm_instance()
182 gist_repo = gist.scm_instance()
198
183
199 lifetime = safe_int(lifetime, -1)
200 if lifetime == 0: # preserve old value
184 if lifetime == 0: # preserve old value
201 gist_expires = gist.gist_expires
185 gist_expires = gist.gist_expires
202 else:
186 else:
203 gist_expires = (
187 gist_expires = (
204 time.time() + (lifetime * 60) if lifetime != -1 else -1)
188 time.time() + (lifetime * 60) if lifetime != -1 else -1)
205
189
206 # calculate operation type based on given data
190 # calculate operation type based on given data
207 gist_mapping_op = {}
191 gist_mapping_op = {}
208 for k, v in gist_mapping.items():
192 for k, v in gist_mapping.items():
209 # add, mod, del
193 # add, mod, del
210 if not v['org_filename'] and v['filename']:
194 if not v['filename_org'] and v['filename']:
211 op = 'add'
195 op = 'add'
212 elif v['org_filename'] and not v['filename']:
196 elif v['filename_org'] and not v['filename']:
213 op = 'del'
197 op = 'del'
214 else:
198 else:
215 op = 'mod'
199 op = 'mod'
216
200
217 v['op'] = op
201 v['op'] = op
218 gist_mapping_op[k] = v
202 gist_mapping_op[k] = v
219
203
220 gist.gist_description = description
204 gist.gist_description = description
221 gist.gist_expires = gist_expires
205 gist.gist_expires = gist_expires
222 gist.owner = owner
206 gist.owner = owner
223 gist.gist_type = gist_type
207 gist.gist_type = gist_type
224 gist.acl_level = gist_acl_level
208 gist.acl_level = gist_acl_level
225 self.sa.add(gist)
209 self.sa.add(gist)
226 self.sa.flush()
210 self.sa.flush()
227
211
228 message = 'updated file'
212 message = 'updated file'
229 message += 's: ' if len(gist_mapping) > 1 else ': '
213 message += 's: ' if len(gist_mapping) > 1 else ': '
230 message += ', '.join([x for x in gist_mapping])
214 message += ', '.join([x for x in gist_mapping])
231
215
232 # fake RhodeCode Repository object
216 # fake RhodeCode Repository object
233 fake_repo = AttributeDict({
217 fake_repo = AttributeDict({
234 'repo_name': gist_repo.path,
218 'repo_name': gist_repo.path,
235 'scm_instance': lambda *args, **kwargs: gist_repo,
219 'scm_instance': lambda *args, **kwargs: gist_repo,
236 })
220 })
237
221
238 self._store_metadata(gist_repo, gist.gist_id, gist.gist_access_id,
222 self._store_metadata(gist_repo, gist.gist_id, gist.gist_access_id,
239 owner.user_id, owner.username, gist.gist_type,
223 owner.user_id, owner.username, gist.gist_type,
240 gist.gist_expires, gist_acl_level)
224 gist.gist_expires, gist_acl_level)
241
225
242 # this can throw NodeNotChangedError, if changes we're trying to commit
226 # this can throw NodeNotChangedError, if changes we're trying to commit
243 # are not actually changes...
227 # are not actually changes...
244 ScmModel().update_nodes(
228 ScmModel().update_nodes(
245 user=owner.user_id,
229 user=owner.user_id,
246 repo=fake_repo,
230 repo=fake_repo,
247 message=message,
231 message=message,
248 nodes=gist_mapping_op,
232 nodes=gist_mapping_op,
249 trigger_push_hook=False
233 trigger_push_hook=False
250 )
234 )
251
235
252 return gist
236 return gist
@@ -1,1125 +1,1089 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Set of generic validators
22 Set of generic validators
23 """
23 """
24
24
25 import logging
25 import logging
26 import os
26 import os
27 import re
27 import re
28 from collections import defaultdict
28 from collections import defaultdict
29
29
30 import formencode
30 import formencode
31 import ipaddress
31 import ipaddress
32 from formencode.validators import (
32 from formencode.validators import (
33 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set,
33 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set,
34 NotEmpty, IPAddress, CIDR, String, FancyValidator
34 NotEmpty, IPAddress, CIDR, String, FancyValidator
35 )
35 )
36 from pylons.i18n.translation import _
36 from pylons.i18n.translation import _
37 from sqlalchemy.sql.expression import true
37 from sqlalchemy.sql.expression import true
38 from sqlalchemy.util import OrderedSet
38 from sqlalchemy.util import OrderedSet
39 from webhelpers.pylonslib.secure_form import authentication_token
39 from webhelpers.pylonslib.secure_form import authentication_token
40
40
41 from rhodecode.authentication import (
41 from rhodecode.authentication import (
42 legacy_plugin_prefix, _import_legacy_plugin)
42 legacy_plugin_prefix, _import_legacy_plugin)
43 from rhodecode.authentication.base import loadplugin
43 from rhodecode.authentication.base import loadplugin
44 from rhodecode.config.routing import ADMIN_PREFIX
44 from rhodecode.config.routing import ADMIN_PREFIX
45 from rhodecode.lib.auth import HasRepoGroupPermissionAny, HasPermissionAny
45 from rhodecode.lib.auth import HasRepoGroupPermissionAny, HasPermissionAny
46 from rhodecode.lib.utils import repo_name_slug, make_db_config
46 from rhodecode.lib.utils import repo_name_slug, make_db_config
47 from rhodecode.lib.utils2 import safe_int, str2bool, aslist, md5
47 from rhodecode.lib.utils2 import safe_int, str2bool, aslist, md5
48 from rhodecode.lib.vcs.backends.git.repository import GitRepository
48 from rhodecode.lib.vcs.backends.git.repository import GitRepository
49 from rhodecode.lib.vcs.backends.hg.repository import MercurialRepository
49 from rhodecode.lib.vcs.backends.hg.repository import MercurialRepository
50 from rhodecode.lib.vcs.backends.svn.repository import SubversionRepository
50 from rhodecode.lib.vcs.backends.svn.repository import SubversionRepository
51 from rhodecode.model.db import (
51 from rhodecode.model.db import (
52 RepoGroup, Repository, UserGroup, User, ChangesetStatus, Gist)
52 RepoGroup, Repository, UserGroup, User, ChangesetStatus, Gist)
53 from rhodecode.model.settings import VcsSettingsModel
53 from rhodecode.model.settings import VcsSettingsModel
54
54
55 # silence warnings and pylint
55 # silence warnings and pylint
56 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \
56 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \
57 NotEmpty, IPAddress, CIDR, String, FancyValidator
57 NotEmpty, IPAddress, CIDR, String, FancyValidator
58
58
59 log = logging.getLogger(__name__)
59 log = logging.getLogger(__name__)
60
60
61
61
62 class _Missing(object):
62 class _Missing(object):
63 pass
63 pass
64
64
65 Missing = _Missing()
65 Missing = _Missing()
66
66
67
67
68 class StateObj(object):
68 class StateObj(object):
69 """
69 """
70 this is needed to translate the messages using _() in validators
70 this is needed to translate the messages using _() in validators
71 """
71 """
72 _ = staticmethod(_)
72 _ = staticmethod(_)
73
73
74
74
75 def M(self, key, state=None, **kwargs):
75 def M(self, key, state=None, **kwargs):
76 """
76 """
77 returns string from self.message based on given key,
77 returns string from self.message based on given key,
78 passed kw params are used to substitute %(named)s params inside
78 passed kw params are used to substitute %(named)s params inside
79 translated strings
79 translated strings
80
80
81 :param msg:
81 :param msg:
82 :param state:
82 :param state:
83 """
83 """
84 if state is None:
84 if state is None:
85 state = StateObj()
85 state = StateObj()
86 else:
86 else:
87 state._ = staticmethod(_)
87 state._ = staticmethod(_)
88 # inject validator into state object
88 # inject validator into state object
89 return self.message(key, state, **kwargs)
89 return self.message(key, state, **kwargs)
90
90
91
91
92 def UniqueList(convert=None):
92 def UniqueList(convert=None):
93 class _UniqueList(formencode.FancyValidator):
93 class _UniqueList(formencode.FancyValidator):
94 """
94 """
95 Unique List !
95 Unique List !
96 """
96 """
97 messages = {
97 messages = {
98 'empty': _(u'Value cannot be an empty list'),
98 'empty': _(u'Value cannot be an empty list'),
99 'missing_value': _(u'Value cannot be an empty list'),
99 'missing_value': _(u'Value cannot be an empty list'),
100 }
100 }
101
101
102 def _to_python(self, value, state):
102 def _to_python(self, value, state):
103 ret_val = []
103 ret_val = []
104
104
105 def make_unique(value):
105 def make_unique(value):
106 seen = []
106 seen = []
107 return [c for c in value if not (c in seen or seen.append(c))]
107 return [c for c in value if not (c in seen or seen.append(c))]
108
108
109 if isinstance(value, list):
109 if isinstance(value, list):
110 ret_val = make_unique(value)
110 ret_val = make_unique(value)
111 elif isinstance(value, set):
111 elif isinstance(value, set):
112 ret_val = make_unique(list(value))
112 ret_val = make_unique(list(value))
113 elif isinstance(value, tuple):
113 elif isinstance(value, tuple):
114 ret_val = make_unique(list(value))
114 ret_val = make_unique(list(value))
115 elif value is None:
115 elif value is None:
116 ret_val = []
116 ret_val = []
117 else:
117 else:
118 ret_val = [value]
118 ret_val = [value]
119
119
120 if convert:
120 if convert:
121 ret_val = map(convert, ret_val)
121 ret_val = map(convert, ret_val)
122 return ret_val
122 return ret_val
123
123
124 def empty_value(self, value):
124 def empty_value(self, value):
125 return []
125 return []
126
126
127 return _UniqueList
127 return _UniqueList
128
128
129
129
130 def UniqueListFromString():
130 def UniqueListFromString():
131 class _UniqueListFromString(UniqueList()):
131 class _UniqueListFromString(UniqueList()):
132 def _to_python(self, value, state):
132 def _to_python(self, value, state):
133 if isinstance(value, basestring):
133 if isinstance(value, basestring):
134 value = aslist(value, ',')
134 value = aslist(value, ',')
135 return super(_UniqueListFromString, self)._to_python(value, state)
135 return super(_UniqueListFromString, self)._to_python(value, state)
136 return _UniqueListFromString
136 return _UniqueListFromString
137
137
138
138
139 def ValidSvnPattern(section, repo_name=None):
139 def ValidSvnPattern(section, repo_name=None):
140 class _validator(formencode.validators.FancyValidator):
140 class _validator(formencode.validators.FancyValidator):
141 messages = {
141 messages = {
142 'pattern_exists': _(u'Pattern already exists'),
142 'pattern_exists': _(u'Pattern already exists'),
143 }
143 }
144
144
145 def validate_python(self, value, state):
145 def validate_python(self, value, state):
146 if not value:
146 if not value:
147 return
147 return
148 model = VcsSettingsModel(repo=repo_name)
148 model = VcsSettingsModel(repo=repo_name)
149 ui_settings = model.get_svn_patterns(section=section)
149 ui_settings = model.get_svn_patterns(section=section)
150 for entry in ui_settings:
150 for entry in ui_settings:
151 if value == entry.value:
151 if value == entry.value:
152 msg = M(self, 'pattern_exists', state)
152 msg = M(self, 'pattern_exists', state)
153 raise formencode.Invalid(msg, value, state)
153 raise formencode.Invalid(msg, value, state)
154 return _validator
154 return _validator
155
155
156
156
157 def ValidUsername(edit=False, old_data={}):
157 def ValidUsername(edit=False, old_data={}):
158 class _validator(formencode.validators.FancyValidator):
158 class _validator(formencode.validators.FancyValidator):
159 messages = {
159 messages = {
160 'username_exists': _(u'Username "%(username)s" already exists'),
160 'username_exists': _(u'Username "%(username)s" already exists'),
161 'system_invalid_username':
161 'system_invalid_username':
162 _(u'Username "%(username)s" is forbidden'),
162 _(u'Username "%(username)s" is forbidden'),
163 'invalid_username':
163 'invalid_username':
164 _(u'Username may only contain alphanumeric characters '
164 _(u'Username may only contain alphanumeric characters '
165 u'underscores, periods or dashes and must begin with '
165 u'underscores, periods or dashes and must begin with '
166 u'alphanumeric character or underscore')
166 u'alphanumeric character or underscore')
167 }
167 }
168
168
169 def validate_python(self, value, state):
169 def validate_python(self, value, state):
170 if value in ['default', 'new_user']:
170 if value in ['default', 'new_user']:
171 msg = M(self, 'system_invalid_username', state, username=value)
171 msg = M(self, 'system_invalid_username', state, username=value)
172 raise formencode.Invalid(msg, value, state)
172 raise formencode.Invalid(msg, value, state)
173 # check if user is unique
173 # check if user is unique
174 old_un = None
174 old_un = None
175 if edit:
175 if edit:
176 old_un = User.get(old_data.get('user_id')).username
176 old_un = User.get(old_data.get('user_id')).username
177
177
178 if old_un != value or not edit:
178 if old_un != value or not edit:
179 if User.get_by_username(value, case_insensitive=True):
179 if User.get_by_username(value, case_insensitive=True):
180 msg = M(self, 'username_exists', state, username=value)
180 msg = M(self, 'username_exists', state, username=value)
181 raise formencode.Invalid(msg, value, state)
181 raise formencode.Invalid(msg, value, state)
182
182
183 if (re.match(r'^[\w]{1}[\w\-\.]{0,254}$', value)
183 if (re.match(r'^[\w]{1}[\w\-\.]{0,254}$', value)
184 is None):
184 is None):
185 msg = M(self, 'invalid_username', state)
185 msg = M(self, 'invalid_username', state)
186 raise formencode.Invalid(msg, value, state)
186 raise formencode.Invalid(msg, value, state)
187 return _validator
187 return _validator
188
188
189
189
190 def ValidRegex(msg=None):
190 def ValidRegex(msg=None):
191 class _validator(formencode.validators.Regex):
191 class _validator(formencode.validators.Regex):
192 messages = {'invalid': msg or _(u'The input is not valid')}
192 messages = {'invalid': msg or _(u'The input is not valid')}
193 return _validator
193 return _validator
194
194
195
195
196 def ValidRepoUser(allow_disabled=False):
196 def ValidRepoUser(allow_disabled=False):
197 class _validator(formencode.validators.FancyValidator):
197 class _validator(formencode.validators.FancyValidator):
198 messages = {
198 messages = {
199 'invalid_username': _(u'Username %(username)s is not valid'),
199 'invalid_username': _(u'Username %(username)s is not valid'),
200 'disabled_username': _(u'Username %(username)s is disabled')
200 'disabled_username': _(u'Username %(username)s is disabled')
201 }
201 }
202
202
203 def validate_python(self, value, state):
203 def validate_python(self, value, state):
204 try:
204 try:
205 user = User.query().filter(User.username == value).one()
205 user = User.query().filter(User.username == value).one()
206 except Exception:
206 except Exception:
207 msg = M(self, 'invalid_username', state, username=value)
207 msg = M(self, 'invalid_username', state, username=value)
208 raise formencode.Invalid(
208 raise formencode.Invalid(
209 msg, value, state, error_dict={'username': msg}
209 msg, value, state, error_dict={'username': msg}
210 )
210 )
211 if user and (not allow_disabled and not user.active):
211 if user and (not allow_disabled and not user.active):
212 msg = M(self, 'disabled_username', state, username=value)
212 msg = M(self, 'disabled_username', state, username=value)
213 raise formencode.Invalid(
213 raise formencode.Invalid(
214 msg, value, state, error_dict={'username': msg}
214 msg, value, state, error_dict={'username': msg}
215 )
215 )
216
216
217 return _validator
217 return _validator
218
218
219
219
220 def ValidUserGroup(edit=False, old_data={}):
220 def ValidUserGroup(edit=False, old_data={}):
221 class _validator(formencode.validators.FancyValidator):
221 class _validator(formencode.validators.FancyValidator):
222 messages = {
222 messages = {
223 'invalid_group': _(u'Invalid user group name'),
223 'invalid_group': _(u'Invalid user group name'),
224 'group_exist': _(u'User group "%(usergroup)s" already exists'),
224 'group_exist': _(u'User group "%(usergroup)s" already exists'),
225 'invalid_usergroup_name':
225 'invalid_usergroup_name':
226 _(u'user group name may only contain alphanumeric '
226 _(u'user group name may only contain alphanumeric '
227 u'characters underscores, periods or dashes and must begin '
227 u'characters underscores, periods or dashes and must begin '
228 u'with alphanumeric character')
228 u'with alphanumeric character')
229 }
229 }
230
230
231 def validate_python(self, value, state):
231 def validate_python(self, value, state):
232 if value in ['default']:
232 if value in ['default']:
233 msg = M(self, 'invalid_group', state)
233 msg = M(self, 'invalid_group', state)
234 raise formencode.Invalid(
234 raise formencode.Invalid(
235 msg, value, state, error_dict={'users_group_name': msg}
235 msg, value, state, error_dict={'users_group_name': msg}
236 )
236 )
237 # check if group is unique
237 # check if group is unique
238 old_ugname = None
238 old_ugname = None
239 if edit:
239 if edit:
240 old_id = old_data.get('users_group_id')
240 old_id = old_data.get('users_group_id')
241 old_ugname = UserGroup.get(old_id).users_group_name
241 old_ugname = UserGroup.get(old_id).users_group_name
242
242
243 if old_ugname != value or not edit:
243 if old_ugname != value or not edit:
244 is_existing_group = UserGroup.get_by_group_name(
244 is_existing_group = UserGroup.get_by_group_name(
245 value, case_insensitive=True)
245 value, case_insensitive=True)
246 if is_existing_group:
246 if is_existing_group:
247 msg = M(self, 'group_exist', state, usergroup=value)
247 msg = M(self, 'group_exist', state, usergroup=value)
248 raise formencode.Invalid(
248 raise formencode.Invalid(
249 msg, value, state, error_dict={'users_group_name': msg}
249 msg, value, state, error_dict={'users_group_name': msg}
250 )
250 )
251
251
252 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
252 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
253 msg = M(self, 'invalid_usergroup_name', state)
253 msg = M(self, 'invalid_usergroup_name', state)
254 raise formencode.Invalid(
254 raise formencode.Invalid(
255 msg, value, state, error_dict={'users_group_name': msg}
255 msg, value, state, error_dict={'users_group_name': msg}
256 )
256 )
257
257
258 return _validator
258 return _validator
259
259
260
260
261 def ValidRepoGroup(edit=False, old_data={}, can_create_in_root=False):
261 def ValidRepoGroup(edit=False, old_data={}, can_create_in_root=False):
262 class _validator(formencode.validators.FancyValidator):
262 class _validator(formencode.validators.FancyValidator):
263 messages = {
263 messages = {
264 'group_parent_id': _(u'Cannot assign this group as parent'),
264 'group_parent_id': _(u'Cannot assign this group as parent'),
265 'group_exists': _(u'Group "%(group_name)s" already exists'),
265 'group_exists': _(u'Group "%(group_name)s" already exists'),
266 'repo_exists': _(u'Repository with name "%(group_name)s" '
266 'repo_exists': _(u'Repository with name "%(group_name)s" '
267 u'already exists'),
267 u'already exists'),
268 'permission_denied': _(u"no permission to store repository group"
268 'permission_denied': _(u"no permission to store repository group"
269 u"in this location"),
269 u"in this location"),
270 'permission_denied_root': _(
270 'permission_denied_root': _(
271 u"no permission to store repository group "
271 u"no permission to store repository group "
272 u"in root location")
272 u"in root location")
273 }
273 }
274
274
275 def _to_python(self, value, state):
275 def _to_python(self, value, state):
276 group_name = repo_name_slug(value.get('group_name', ''))
276 group_name = repo_name_slug(value.get('group_name', ''))
277 group_parent_id = safe_int(value.get('group_parent_id'))
277 group_parent_id = safe_int(value.get('group_parent_id'))
278 gr = RepoGroup.get(group_parent_id)
278 gr = RepoGroup.get(group_parent_id)
279 if gr:
279 if gr:
280 parent_group_path = gr.full_path
280 parent_group_path = gr.full_path
281 # value needs to be aware of group name in order to check
281 # value needs to be aware of group name in order to check
282 # db key This is an actual just the name to store in the
282 # db key This is an actual just the name to store in the
283 # database
283 # database
284 group_name_full = (
284 group_name_full = (
285 parent_group_path + RepoGroup.url_sep() + group_name)
285 parent_group_path + RepoGroup.url_sep() + group_name)
286 else:
286 else:
287 group_name_full = group_name
287 group_name_full = group_name
288
288
289 value['group_name'] = group_name
289 value['group_name'] = group_name
290 value['group_name_full'] = group_name_full
290 value['group_name_full'] = group_name_full
291 value['group_parent_id'] = group_parent_id
291 value['group_parent_id'] = group_parent_id
292 return value
292 return value
293
293
294 def validate_python(self, value, state):
294 def validate_python(self, value, state):
295
295
296 old_group_name = None
296 old_group_name = None
297 group_name = value.get('group_name')
297 group_name = value.get('group_name')
298 group_name_full = value.get('group_name_full')
298 group_name_full = value.get('group_name_full')
299 group_parent_id = safe_int(value.get('group_parent_id'))
299 group_parent_id = safe_int(value.get('group_parent_id'))
300 if group_parent_id == -1:
300 if group_parent_id == -1:
301 group_parent_id = None
301 group_parent_id = None
302
302
303 group_obj = RepoGroup.get(old_data.get('group_id'))
303 group_obj = RepoGroup.get(old_data.get('group_id'))
304 parent_group_changed = False
304 parent_group_changed = False
305 if edit:
305 if edit:
306 old_group_name = group_obj.group_name
306 old_group_name = group_obj.group_name
307 old_group_parent_id = group_obj.group_parent_id
307 old_group_parent_id = group_obj.group_parent_id
308
308
309 if group_parent_id != old_group_parent_id:
309 if group_parent_id != old_group_parent_id:
310 parent_group_changed = True
310 parent_group_changed = True
311
311
312 # TODO: mikhail: the following if statement is not reached
312 # TODO: mikhail: the following if statement is not reached
313 # since group_parent_id's OneOf validation fails before.
313 # since group_parent_id's OneOf validation fails before.
314 # Can be removed.
314 # Can be removed.
315
315
316 # check against setting a parent of self
316 # check against setting a parent of self
317 parent_of_self = (
317 parent_of_self = (
318 old_data['group_id'] == group_parent_id
318 old_data['group_id'] == group_parent_id
319 if group_parent_id else False
319 if group_parent_id else False
320 )
320 )
321 if parent_of_self:
321 if parent_of_self:
322 msg = M(self, 'group_parent_id', state)
322 msg = M(self, 'group_parent_id', state)
323 raise formencode.Invalid(
323 raise formencode.Invalid(
324 msg, value, state, error_dict={'group_parent_id': msg}
324 msg, value, state, error_dict={'group_parent_id': msg}
325 )
325 )
326
326
327 # group we're moving current group inside
327 # group we're moving current group inside
328 child_group = None
328 child_group = None
329 if group_parent_id:
329 if group_parent_id:
330 child_group = RepoGroup.query().filter(
330 child_group = RepoGroup.query().filter(
331 RepoGroup.group_id == group_parent_id).scalar()
331 RepoGroup.group_id == group_parent_id).scalar()
332
332
333 # do a special check that we cannot move a group to one of
333 # do a special check that we cannot move a group to one of
334 # it's children
334 # it's children
335 if edit and child_group:
335 if edit and child_group:
336 parents = [x.group_id for x in child_group.parents]
336 parents = [x.group_id for x in child_group.parents]
337 move_to_children = old_data['group_id'] in parents
337 move_to_children = old_data['group_id'] in parents
338 if move_to_children:
338 if move_to_children:
339 msg = M(self, 'group_parent_id', state)
339 msg = M(self, 'group_parent_id', state)
340 raise formencode.Invalid(
340 raise formencode.Invalid(
341 msg, value, state, error_dict={'group_parent_id': msg})
341 msg, value, state, error_dict={'group_parent_id': msg})
342
342
343 # Check if we have permission to store in the parent.
343 # Check if we have permission to store in the parent.
344 # Only check if the parent group changed.
344 # Only check if the parent group changed.
345 if parent_group_changed:
345 if parent_group_changed:
346 if child_group is None:
346 if child_group is None:
347 if not can_create_in_root:
347 if not can_create_in_root:
348 msg = M(self, 'permission_denied_root', state)
348 msg = M(self, 'permission_denied_root', state)
349 raise formencode.Invalid(
349 raise formencode.Invalid(
350 msg, value, state,
350 msg, value, state,
351 error_dict={'group_parent_id': msg})
351 error_dict={'group_parent_id': msg})
352 else:
352 else:
353 valid = HasRepoGroupPermissionAny('group.admin')
353 valid = HasRepoGroupPermissionAny('group.admin')
354 forbidden = not valid(
354 forbidden = not valid(
355 child_group.group_name, 'can create group validator')
355 child_group.group_name, 'can create group validator')
356 if forbidden:
356 if forbidden:
357 msg = M(self, 'permission_denied', state)
357 msg = M(self, 'permission_denied', state)
358 raise formencode.Invalid(
358 raise formencode.Invalid(
359 msg, value, state,
359 msg, value, state,
360 error_dict={'group_parent_id': msg})
360 error_dict={'group_parent_id': msg})
361
361
362 # if we change the name or it's new group, check for existing names
362 # if we change the name or it's new group, check for existing names
363 # or repositories with the same name
363 # or repositories with the same name
364 if old_group_name != group_name_full or not edit:
364 if old_group_name != group_name_full or not edit:
365 # check group
365 # check group
366 gr = RepoGroup.get_by_group_name(group_name_full)
366 gr = RepoGroup.get_by_group_name(group_name_full)
367 if gr:
367 if gr:
368 msg = M(self, 'group_exists', state, group_name=group_name)
368 msg = M(self, 'group_exists', state, group_name=group_name)
369 raise formencode.Invalid(
369 raise formencode.Invalid(
370 msg, value, state, error_dict={'group_name': msg})
370 msg, value, state, error_dict={'group_name': msg})
371
371
372 # check for same repo
372 # check for same repo
373 repo = Repository.get_by_repo_name(group_name_full)
373 repo = Repository.get_by_repo_name(group_name_full)
374 if repo:
374 if repo:
375 msg = M(self, 'repo_exists', state, group_name=group_name)
375 msg = M(self, 'repo_exists', state, group_name=group_name)
376 raise formencode.Invalid(
376 raise formencode.Invalid(
377 msg, value, state, error_dict={'group_name': msg})
377 msg, value, state, error_dict={'group_name': msg})
378
378
379 return _validator
379 return _validator
380
380
381
381
382 def ValidPassword():
382 def ValidPassword():
383 class _validator(formencode.validators.FancyValidator):
383 class _validator(formencode.validators.FancyValidator):
384 messages = {
384 messages = {
385 'invalid_password':
385 'invalid_password':
386 _(u'Invalid characters (non-ascii) in password')
386 _(u'Invalid characters (non-ascii) in password')
387 }
387 }
388
388
389 def validate_python(self, value, state):
389 def validate_python(self, value, state):
390 try:
390 try:
391 (value or '').decode('ascii')
391 (value or '').decode('ascii')
392 except UnicodeError:
392 except UnicodeError:
393 msg = M(self, 'invalid_password', state)
393 msg = M(self, 'invalid_password', state)
394 raise formencode.Invalid(msg, value, state,)
394 raise formencode.Invalid(msg, value, state,)
395 return _validator
395 return _validator
396
396
397
397
398 def ValidOldPassword(username):
398 def ValidOldPassword(username):
399 class _validator(formencode.validators.FancyValidator):
399 class _validator(formencode.validators.FancyValidator):
400 messages = {
400 messages = {
401 'invalid_password': _(u'Invalid old password')
401 'invalid_password': _(u'Invalid old password')
402 }
402 }
403
403
404 def validate_python(self, value, state):
404 def validate_python(self, value, state):
405 from rhodecode.authentication.base import authenticate, HTTP_TYPE
405 from rhodecode.authentication.base import authenticate, HTTP_TYPE
406 if not authenticate(username, value, '', HTTP_TYPE):
406 if not authenticate(username, value, '', HTTP_TYPE):
407 msg = M(self, 'invalid_password', state)
407 msg = M(self, 'invalid_password', state)
408 raise formencode.Invalid(
408 raise formencode.Invalid(
409 msg, value, state, error_dict={'current_password': msg}
409 msg, value, state, error_dict={'current_password': msg}
410 )
410 )
411 return _validator
411 return _validator
412
412
413
413
414 def ValidPasswordsMatch(
414 def ValidPasswordsMatch(
415 passwd='new_password', passwd_confirmation='password_confirmation'):
415 passwd='new_password', passwd_confirmation='password_confirmation'):
416 class _validator(formencode.validators.FancyValidator):
416 class _validator(formencode.validators.FancyValidator):
417 messages = {
417 messages = {
418 'password_mismatch': _(u'Passwords do not match'),
418 'password_mismatch': _(u'Passwords do not match'),
419 }
419 }
420
420
421 def validate_python(self, value, state):
421 def validate_python(self, value, state):
422
422
423 pass_val = value.get('password') or value.get(passwd)
423 pass_val = value.get('password') or value.get(passwd)
424 if pass_val != value[passwd_confirmation]:
424 if pass_val != value[passwd_confirmation]:
425 msg = M(self, 'password_mismatch', state)
425 msg = M(self, 'password_mismatch', state)
426 raise formencode.Invalid(
426 raise formencode.Invalid(
427 msg, value, state,
427 msg, value, state,
428 error_dict={passwd: msg, passwd_confirmation: msg}
428 error_dict={passwd: msg, passwd_confirmation: msg}
429 )
429 )
430 return _validator
430 return _validator
431
431
432
432
433 def ValidAuth():
433 def ValidAuth():
434 class _validator(formencode.validators.FancyValidator):
434 class _validator(formencode.validators.FancyValidator):
435 messages = {
435 messages = {
436 'invalid_password': _(u'invalid password'),
436 'invalid_password': _(u'invalid password'),
437 'invalid_username': _(u'invalid user name'),
437 'invalid_username': _(u'invalid user name'),
438 'disabled_account': _(u'Your account is disabled')
438 'disabled_account': _(u'Your account is disabled')
439 }
439 }
440
440
441 def validate_python(self, value, state):
441 def validate_python(self, value, state):
442 from rhodecode.authentication.base import authenticate, HTTP_TYPE
442 from rhodecode.authentication.base import authenticate, HTTP_TYPE
443
443
444 password = value['password']
444 password = value['password']
445 username = value['username']
445 username = value['username']
446
446
447 if not authenticate(username, password, '', HTTP_TYPE,
447 if not authenticate(username, password, '', HTTP_TYPE,
448 skip_missing=True):
448 skip_missing=True):
449 user = User.get_by_username(username)
449 user = User.get_by_username(username)
450 if user and not user.active:
450 if user and not user.active:
451 log.warning('user %s is disabled', username)
451 log.warning('user %s is disabled', username)
452 msg = M(self, 'disabled_account', state)
452 msg = M(self, 'disabled_account', state)
453 raise formencode.Invalid(
453 raise formencode.Invalid(
454 msg, value, state, error_dict={'username': msg}
454 msg, value, state, error_dict={'username': msg}
455 )
455 )
456 else:
456 else:
457 log.warning('user `%s` failed to authenticate', username)
457 log.warning('user `%s` failed to authenticate', username)
458 msg = M(self, 'invalid_username', state)
458 msg = M(self, 'invalid_username', state)
459 msg2 = M(self, 'invalid_password', state)
459 msg2 = M(self, 'invalid_password', state)
460 raise formencode.Invalid(
460 raise formencode.Invalid(
461 msg, value, state,
461 msg, value, state,
462 error_dict={'username': msg, 'password': msg2}
462 error_dict={'username': msg, 'password': msg2}
463 )
463 )
464 return _validator
464 return _validator
465
465
466
466
467 def ValidAuthToken():
467 def ValidAuthToken():
468 class _validator(formencode.validators.FancyValidator):
468 class _validator(formencode.validators.FancyValidator):
469 messages = {
469 messages = {
470 'invalid_token': _(u'Token mismatch')
470 'invalid_token': _(u'Token mismatch')
471 }
471 }
472
472
473 def validate_python(self, value, state):
473 def validate_python(self, value, state):
474 if value != authentication_token():
474 if value != authentication_token():
475 msg = M(self, 'invalid_token', state)
475 msg = M(self, 'invalid_token', state)
476 raise formencode.Invalid(msg, value, state)
476 raise formencode.Invalid(msg, value, state)
477 return _validator
477 return _validator
478
478
479
479
480 def ValidRepoName(edit=False, old_data={}):
480 def ValidRepoName(edit=False, old_data={}):
481 class _validator(formencode.validators.FancyValidator):
481 class _validator(formencode.validators.FancyValidator):
482 messages = {
482 messages = {
483 'invalid_repo_name':
483 'invalid_repo_name':
484 _(u'Repository name %(repo)s is disallowed'),
484 _(u'Repository name %(repo)s is disallowed'),
485 # top level
485 # top level
486 'repository_exists': _(u'Repository with name %(repo)s '
486 'repository_exists': _(u'Repository with name %(repo)s '
487 u'already exists'),
487 u'already exists'),
488 'group_exists': _(u'Repository group with name "%(repo)s" '
488 'group_exists': _(u'Repository group with name "%(repo)s" '
489 u'already exists'),
489 u'already exists'),
490 # inside a group
490 # inside a group
491 'repository_in_group_exists': _(u'Repository with name %(repo)s '
491 'repository_in_group_exists': _(u'Repository with name %(repo)s '
492 u'exists in group "%(group)s"'),
492 u'exists in group "%(group)s"'),
493 'group_in_group_exists': _(
493 'group_in_group_exists': _(
494 u'Repository group with name "%(repo)s" '
494 u'Repository group with name "%(repo)s" '
495 u'exists in group "%(group)s"'),
495 u'exists in group "%(group)s"'),
496 }
496 }
497
497
498 def _to_python(self, value, state):
498 def _to_python(self, value, state):
499 repo_name = repo_name_slug(value.get('repo_name', ''))
499 repo_name = repo_name_slug(value.get('repo_name', ''))
500 repo_group = value.get('repo_group')
500 repo_group = value.get('repo_group')
501 if repo_group:
501 if repo_group:
502 gr = RepoGroup.get(repo_group)
502 gr = RepoGroup.get(repo_group)
503 group_path = gr.full_path
503 group_path = gr.full_path
504 group_name = gr.group_name
504 group_name = gr.group_name
505 # value needs to be aware of group name in order to check
505 # value needs to be aware of group name in order to check
506 # db key This is an actual just the name to store in the
506 # db key This is an actual just the name to store in the
507 # database
507 # database
508 repo_name_full = group_path + RepoGroup.url_sep() + repo_name
508 repo_name_full = group_path + RepoGroup.url_sep() + repo_name
509 else:
509 else:
510 group_name = group_path = ''
510 group_name = group_path = ''
511 repo_name_full = repo_name
511 repo_name_full = repo_name
512
512
513 value['repo_name'] = repo_name
513 value['repo_name'] = repo_name
514 value['repo_name_full'] = repo_name_full
514 value['repo_name_full'] = repo_name_full
515 value['group_path'] = group_path
515 value['group_path'] = group_path
516 value['group_name'] = group_name
516 value['group_name'] = group_name
517 return value
517 return value
518
518
519 def validate_python(self, value, state):
519 def validate_python(self, value, state):
520
520
521 repo_name = value.get('repo_name')
521 repo_name = value.get('repo_name')
522 repo_name_full = value.get('repo_name_full')
522 repo_name_full = value.get('repo_name_full')
523 group_path = value.get('group_path')
523 group_path = value.get('group_path')
524 group_name = value.get('group_name')
524 group_name = value.get('group_name')
525
525
526 if repo_name in [ADMIN_PREFIX, '']:
526 if repo_name in [ADMIN_PREFIX, '']:
527 msg = M(self, 'invalid_repo_name', state, repo=repo_name)
527 msg = M(self, 'invalid_repo_name', state, repo=repo_name)
528 raise formencode.Invalid(
528 raise formencode.Invalid(
529 msg, value, state, error_dict={'repo_name': msg})
529 msg, value, state, error_dict={'repo_name': msg})
530
530
531 rename = old_data.get('repo_name') != repo_name_full
531 rename = old_data.get('repo_name') != repo_name_full
532 create = not edit
532 create = not edit
533 if rename or create:
533 if rename or create:
534
534
535 if group_path:
535 if group_path:
536 if Repository.get_by_repo_name(repo_name_full):
536 if Repository.get_by_repo_name(repo_name_full):
537 msg = M(self, 'repository_in_group_exists', state,
537 msg = M(self, 'repository_in_group_exists', state,
538 repo=repo_name, group=group_name)
538 repo=repo_name, group=group_name)
539 raise formencode.Invalid(
539 raise formencode.Invalid(
540 msg, value, state, error_dict={'repo_name': msg})
540 msg, value, state, error_dict={'repo_name': msg})
541 if RepoGroup.get_by_group_name(repo_name_full):
541 if RepoGroup.get_by_group_name(repo_name_full):
542 msg = M(self, 'group_in_group_exists', state,
542 msg = M(self, 'group_in_group_exists', state,
543 repo=repo_name, group=group_name)
543 repo=repo_name, group=group_name)
544 raise formencode.Invalid(
544 raise formencode.Invalid(
545 msg, value, state, error_dict={'repo_name': msg})
545 msg, value, state, error_dict={'repo_name': msg})
546 else:
546 else:
547 if RepoGroup.get_by_group_name(repo_name_full):
547 if RepoGroup.get_by_group_name(repo_name_full):
548 msg = M(self, 'group_exists', state, repo=repo_name)
548 msg = M(self, 'group_exists', state, repo=repo_name)
549 raise formencode.Invalid(
549 raise formencode.Invalid(
550 msg, value, state, error_dict={'repo_name': msg})
550 msg, value, state, error_dict={'repo_name': msg})
551
551
552 if Repository.get_by_repo_name(repo_name_full):
552 if Repository.get_by_repo_name(repo_name_full):
553 msg = M(
553 msg = M(
554 self, 'repository_exists', state, repo=repo_name)
554 self, 'repository_exists', state, repo=repo_name)
555 raise formencode.Invalid(
555 raise formencode.Invalid(
556 msg, value, state, error_dict={'repo_name': msg})
556 msg, value, state, error_dict={'repo_name': msg})
557 return value
557 return value
558 return _validator
558 return _validator
559
559
560
560
561 def ValidForkName(*args, **kwargs):
561 def ValidForkName(*args, **kwargs):
562 return ValidRepoName(*args, **kwargs)
562 return ValidRepoName(*args, **kwargs)
563
563
564
564
565 def SlugifyName():
565 def SlugifyName():
566 class _validator(formencode.validators.FancyValidator):
566 class _validator(formencode.validators.FancyValidator):
567
567
568 def _to_python(self, value, state):
568 def _to_python(self, value, state):
569 return repo_name_slug(value)
569 return repo_name_slug(value)
570
570
571 def validate_python(self, value, state):
571 def validate_python(self, value, state):
572 pass
572 pass
573
573
574 return _validator
574 return _validator
575
575
576
576
577 def ValidCloneUri():
577 def ValidCloneUri():
578 class InvalidCloneUrl(Exception):
578 class InvalidCloneUrl(Exception):
579 allowed_prefixes = ()
579 allowed_prefixes = ()
580
580
581 def url_handler(repo_type, url):
581 def url_handler(repo_type, url):
582 config = make_db_config(clear_session=False)
582 config = make_db_config(clear_session=False)
583 if repo_type == 'hg':
583 if repo_type == 'hg':
584 allowed_prefixes = ('http', 'svn+http', 'git+http')
584 allowed_prefixes = ('http', 'svn+http', 'git+http')
585
585
586 if 'http' in url[:4]:
586 if 'http' in url[:4]:
587 # initially check if it's at least the proper URL
587 # initially check if it's at least the proper URL
588 # or does it pass basic auth
588 # or does it pass basic auth
589 MercurialRepository.check_url(url, config)
589 MercurialRepository.check_url(url, config)
590 elif 'svn+http' in url[:8]: # svn->hg import
590 elif 'svn+http' in url[:8]: # svn->hg import
591 SubversionRepository.check_url(url, config)
591 SubversionRepository.check_url(url, config)
592 elif 'git+http' in url[:8]: # git->hg import
592 elif 'git+http' in url[:8]: # git->hg import
593 raise NotImplementedError()
593 raise NotImplementedError()
594 else:
594 else:
595 exc = InvalidCloneUrl('Clone from URI %s not allowed. '
595 exc = InvalidCloneUrl('Clone from URI %s not allowed. '
596 'Allowed url must start with one of %s'
596 'Allowed url must start with one of %s'
597 % (url, ','.join(allowed_prefixes)))
597 % (url, ','.join(allowed_prefixes)))
598 exc.allowed_prefixes = allowed_prefixes
598 exc.allowed_prefixes = allowed_prefixes
599 raise exc
599 raise exc
600
600
601 elif repo_type == 'git':
601 elif repo_type == 'git':
602 allowed_prefixes = ('http', 'svn+http', 'hg+http')
602 allowed_prefixes = ('http', 'svn+http', 'hg+http')
603 if 'http' in url[:4]:
603 if 'http' in url[:4]:
604 # initially check if it's at least the proper URL
604 # initially check if it's at least the proper URL
605 # or does it pass basic auth
605 # or does it pass basic auth
606 GitRepository.check_url(url, config)
606 GitRepository.check_url(url, config)
607 elif 'svn+http' in url[:8]: # svn->git import
607 elif 'svn+http' in url[:8]: # svn->git import
608 raise NotImplementedError()
608 raise NotImplementedError()
609 elif 'hg+http' in url[:8]: # hg->git import
609 elif 'hg+http' in url[:8]: # hg->git import
610 raise NotImplementedError()
610 raise NotImplementedError()
611 else:
611 else:
612 exc = InvalidCloneUrl('Clone from URI %s not allowed. '
612 exc = InvalidCloneUrl('Clone from URI %s not allowed. '
613 'Allowed url must start with one of %s'
613 'Allowed url must start with one of %s'
614 % (url, ','.join(allowed_prefixes)))
614 % (url, ','.join(allowed_prefixes)))
615 exc.allowed_prefixes = allowed_prefixes
615 exc.allowed_prefixes = allowed_prefixes
616 raise exc
616 raise exc
617
617
618 class _validator(formencode.validators.FancyValidator):
618 class _validator(formencode.validators.FancyValidator):
619 messages = {
619 messages = {
620 'clone_uri': _(u'invalid clone url for %(rtype)s repository'),
620 'clone_uri': _(u'invalid clone url for %(rtype)s repository'),
621 'invalid_clone_uri': _(
621 'invalid_clone_uri': _(
622 u'Invalid clone url, provide a valid clone '
622 u'Invalid clone url, provide a valid clone '
623 u'url starting with one of %(allowed_prefixes)s')
623 u'url starting with one of %(allowed_prefixes)s')
624 }
624 }
625
625
626 def validate_python(self, value, state):
626 def validate_python(self, value, state):
627 repo_type = value.get('repo_type')
627 repo_type = value.get('repo_type')
628 url = value.get('clone_uri')
628 url = value.get('clone_uri')
629
629
630 if url:
630 if url:
631 try:
631 try:
632 url_handler(repo_type, url)
632 url_handler(repo_type, url)
633 except InvalidCloneUrl as e:
633 except InvalidCloneUrl as e:
634 log.warning(e)
634 log.warning(e)
635 msg = M(self, 'invalid_clone_uri', rtype=repo_type,
635 msg = M(self, 'invalid_clone_uri', rtype=repo_type,
636 allowed_prefixes=','.join(e.allowed_prefixes))
636 allowed_prefixes=','.join(e.allowed_prefixes))
637 raise formencode.Invalid(msg, value, state,
637 raise formencode.Invalid(msg, value, state,
638 error_dict={'clone_uri': msg})
638 error_dict={'clone_uri': msg})
639 except Exception:
639 except Exception:
640 log.exception('Url validation failed')
640 log.exception('Url validation failed')
641 msg = M(self, 'clone_uri', rtype=repo_type)
641 msg = M(self, 'clone_uri', rtype=repo_type)
642 raise formencode.Invalid(msg, value, state,
642 raise formencode.Invalid(msg, value, state,
643 error_dict={'clone_uri': msg})
643 error_dict={'clone_uri': msg})
644 return _validator
644 return _validator
645
645
646
646
647 def ValidForkType(old_data={}):
647 def ValidForkType(old_data={}):
648 class _validator(formencode.validators.FancyValidator):
648 class _validator(formencode.validators.FancyValidator):
649 messages = {
649 messages = {
650 'invalid_fork_type': _(u'Fork have to be the same type as parent')
650 'invalid_fork_type': _(u'Fork have to be the same type as parent')
651 }
651 }
652
652
653 def validate_python(self, value, state):
653 def validate_python(self, value, state):
654 if old_data['repo_type'] != value:
654 if old_data['repo_type'] != value:
655 msg = M(self, 'invalid_fork_type', state)
655 msg = M(self, 'invalid_fork_type', state)
656 raise formencode.Invalid(
656 raise formencode.Invalid(
657 msg, value, state, error_dict={'repo_type': msg}
657 msg, value, state, error_dict={'repo_type': msg}
658 )
658 )
659 return _validator
659 return _validator
660
660
661
661
662 def CanWriteGroup(old_data=None):
662 def CanWriteGroup(old_data=None):
663 class _validator(formencode.validators.FancyValidator):
663 class _validator(formencode.validators.FancyValidator):
664 messages = {
664 messages = {
665 'permission_denied': _(
665 'permission_denied': _(
666 u"You do not have the permission "
666 u"You do not have the permission "
667 u"to create repositories in this group."),
667 u"to create repositories in this group."),
668 'permission_denied_root': _(
668 'permission_denied_root': _(
669 u"You do not have the permission to store repositories in "
669 u"You do not have the permission to store repositories in "
670 u"the root location.")
670 u"the root location.")
671 }
671 }
672
672
673 def _to_python(self, value, state):
673 def _to_python(self, value, state):
674 # root location
674 # root location
675 if value in [-1, "-1"]:
675 if value in [-1, "-1"]:
676 return None
676 return None
677 return value
677 return value
678
678
679 def validate_python(self, value, state):
679 def validate_python(self, value, state):
680 gr = RepoGroup.get(value)
680 gr = RepoGroup.get(value)
681 gr_name = gr.group_name if gr else None # None means ROOT location
681 gr_name = gr.group_name if gr else None # None means ROOT location
682 # create repositories with write permission on group is set to true
682 # create repositories with write permission on group is set to true
683 create_on_write = HasPermissionAny(
683 create_on_write = HasPermissionAny(
684 'hg.create.write_on_repogroup.true')()
684 'hg.create.write_on_repogroup.true')()
685 group_admin = HasRepoGroupPermissionAny('group.admin')(
685 group_admin = HasRepoGroupPermissionAny('group.admin')(
686 gr_name, 'can write into group validator')
686 gr_name, 'can write into group validator')
687 group_write = HasRepoGroupPermissionAny('group.write')(
687 group_write = HasRepoGroupPermissionAny('group.write')(
688 gr_name, 'can write into group validator')
688 gr_name, 'can write into group validator')
689 forbidden = not (group_admin or (group_write and create_on_write))
689 forbidden = not (group_admin or (group_write and create_on_write))
690 can_create_repos = HasPermissionAny(
690 can_create_repos = HasPermissionAny(
691 'hg.admin', 'hg.create.repository')
691 'hg.admin', 'hg.create.repository')
692 gid = (old_data['repo_group'].get('group_id')
692 gid = (old_data['repo_group'].get('group_id')
693 if (old_data and 'repo_group' in old_data) else None)
693 if (old_data and 'repo_group' in old_data) else None)
694 value_changed = gid != safe_int(value)
694 value_changed = gid != safe_int(value)
695 new = not old_data
695 new = not old_data
696 # do check if we changed the value, there's a case that someone got
696 # do check if we changed the value, there's a case that someone got
697 # revoked write permissions to a repository, he still created, we
697 # revoked write permissions to a repository, he still created, we
698 # don't need to check permission if he didn't change the value of
698 # don't need to check permission if he didn't change the value of
699 # groups in form box
699 # groups in form box
700 if value_changed or new:
700 if value_changed or new:
701 # parent group need to be existing
701 # parent group need to be existing
702 if gr and forbidden:
702 if gr and forbidden:
703 msg = M(self, 'permission_denied', state)
703 msg = M(self, 'permission_denied', state)
704 raise formencode.Invalid(
704 raise formencode.Invalid(
705 msg, value, state, error_dict={'repo_type': msg}
705 msg, value, state, error_dict={'repo_type': msg}
706 )
706 )
707 # check if we can write to root location !
707 # check if we can write to root location !
708 elif gr is None and not can_create_repos():
708 elif gr is None and not can_create_repos():
709 msg = M(self, 'permission_denied_root', state)
709 msg = M(self, 'permission_denied_root', state)
710 raise formencode.Invalid(
710 raise formencode.Invalid(
711 msg, value, state, error_dict={'repo_type': msg}
711 msg, value, state, error_dict={'repo_type': msg}
712 )
712 )
713
713
714 return _validator
714 return _validator
715
715
716
716
717 def ValidPerms(type_='repo'):
717 def ValidPerms(type_='repo'):
718 if type_ == 'repo_group':
718 if type_ == 'repo_group':
719 EMPTY_PERM = 'group.none'
719 EMPTY_PERM = 'group.none'
720 elif type_ == 'repo':
720 elif type_ == 'repo':
721 EMPTY_PERM = 'repository.none'
721 EMPTY_PERM = 'repository.none'
722 elif type_ == 'user_group':
722 elif type_ == 'user_group':
723 EMPTY_PERM = 'usergroup.none'
723 EMPTY_PERM = 'usergroup.none'
724
724
725 class _validator(formencode.validators.FancyValidator):
725 class _validator(formencode.validators.FancyValidator):
726 messages = {
726 messages = {
727 'perm_new_member_name':
727 'perm_new_member_name':
728 _(u'This username or user group name is not valid')
728 _(u'This username or user group name is not valid')
729 }
729 }
730
730
731 def _to_python(self, value, state):
731 def _to_python(self, value, state):
732 perm_updates = OrderedSet()
732 perm_updates = OrderedSet()
733 perm_additions = OrderedSet()
733 perm_additions = OrderedSet()
734 perm_deletions = OrderedSet()
734 perm_deletions = OrderedSet()
735 # build a list of permission to update/delete and new permission
735 # build a list of permission to update/delete and new permission
736
736
737 # Read the perm_new_member/perm_del_member attributes and group
737 # Read the perm_new_member/perm_del_member attributes and group
738 # them by they IDs
738 # them by they IDs
739 new_perms_group = defaultdict(dict)
739 new_perms_group = defaultdict(dict)
740 del_perms_group = defaultdict(dict)
740 del_perms_group = defaultdict(dict)
741 for k, v in value.copy().iteritems():
741 for k, v in value.copy().iteritems():
742 if k.startswith('perm_del_member'):
742 if k.startswith('perm_del_member'):
743 # delete from org storage so we don't process that later
743 # delete from org storage so we don't process that later
744 del value[k]
744 del value[k]
745 # part is `id`, `type`
745 # part is `id`, `type`
746 _type, part = k.split('perm_del_member_')
746 _type, part = k.split('perm_del_member_')
747 args = part.split('_')
747 args = part.split('_')
748 if len(args) == 2:
748 if len(args) == 2:
749 _key, pos = args
749 _key, pos = args
750 del_perms_group[pos][_key] = v
750 del_perms_group[pos][_key] = v
751 if k.startswith('perm_new_member'):
751 if k.startswith('perm_new_member'):
752 # delete from org storage so we don't process that later
752 # delete from org storage so we don't process that later
753 del value[k]
753 del value[k]
754 # part is `id`, `type`, `perm`
754 # part is `id`, `type`, `perm`
755 _type, part = k.split('perm_new_member_')
755 _type, part = k.split('perm_new_member_')
756 args = part.split('_')
756 args = part.split('_')
757 if len(args) == 2:
757 if len(args) == 2:
758 _key, pos = args
758 _key, pos = args
759 new_perms_group[pos][_key] = v
759 new_perms_group[pos][_key] = v
760
760
761 # store the deletes
761 # store the deletes
762 for k in sorted(del_perms_group.keys()):
762 for k in sorted(del_perms_group.keys()):
763 perm_dict = del_perms_group[k]
763 perm_dict = del_perms_group[k]
764 del_member = perm_dict.get('id')
764 del_member = perm_dict.get('id')
765 del_type = perm_dict.get('type')
765 del_type = perm_dict.get('type')
766 if del_member and del_type:
766 if del_member and del_type:
767 perm_deletions.add((del_member, None, del_type))
767 perm_deletions.add((del_member, None, del_type))
768
768
769 # store additions in order of how they were added in web form
769 # store additions in order of how they were added in web form
770 for k in sorted(new_perms_group.keys()):
770 for k in sorted(new_perms_group.keys()):
771 perm_dict = new_perms_group[k]
771 perm_dict = new_perms_group[k]
772 new_member = perm_dict.get('id')
772 new_member = perm_dict.get('id')
773 new_type = perm_dict.get('type')
773 new_type = perm_dict.get('type')
774 new_perm = perm_dict.get('perm')
774 new_perm = perm_dict.get('perm')
775 if new_member and new_perm and new_type:
775 if new_member and new_perm and new_type:
776 perm_additions.add((new_member, new_perm, new_type))
776 perm_additions.add((new_member, new_perm, new_type))
777
777
778 # get updates of permissions
778 # get updates of permissions
779 # (read the existing radio button states)
779 # (read the existing radio button states)
780 for k, update_value in value.iteritems():
780 for k, update_value in value.iteritems():
781 if k.startswith('u_perm_') or k.startswith('g_perm_'):
781 if k.startswith('u_perm_') or k.startswith('g_perm_'):
782 member = k[7:]
782 member = k[7:]
783 update_type = {'u': 'user',
783 update_type = {'u': 'user',
784 'g': 'users_group'}[k[0]]
784 'g': 'users_group'}[k[0]]
785 if member == User.DEFAULT_USER:
785 if member == User.DEFAULT_USER:
786 if str2bool(value.get('repo_private')):
786 if str2bool(value.get('repo_private')):
787 # set none for default when updating to
787 # set none for default when updating to
788 # private repo protects agains form manipulation
788 # private repo protects agains form manipulation
789 update_value = EMPTY_PERM
789 update_value = EMPTY_PERM
790 perm_updates.add((member, update_value, update_type))
790 perm_updates.add((member, update_value, update_type))
791 # check the deletes
791 # check the deletes
792
792
793 value['perm_additions'] = list(perm_additions)
793 value['perm_additions'] = list(perm_additions)
794 value['perm_updates'] = list(perm_updates)
794 value['perm_updates'] = list(perm_updates)
795 value['perm_deletions'] = list(perm_deletions)
795 value['perm_deletions'] = list(perm_deletions)
796
796
797 # validate users they exist and they are active !
797 # validate users they exist and they are active !
798 for member_id, _perm, member_type in perm_additions:
798 for member_id, _perm, member_type in perm_additions:
799 try:
799 try:
800 if member_type == 'user':
800 if member_type == 'user':
801 self.user_db = User.query()\
801 self.user_db = User.query()\
802 .filter(User.active == true())\
802 .filter(User.active == true())\
803 .filter(User.user_id == member_id).one()
803 .filter(User.user_id == member_id).one()
804 if member_type == 'users_group':
804 if member_type == 'users_group':
805 self.user_db = UserGroup.query()\
805 self.user_db = UserGroup.query()\
806 .filter(UserGroup.users_group_active == true())\
806 .filter(UserGroup.users_group_active == true())\
807 .filter(UserGroup.users_group_id == member_id)\
807 .filter(UserGroup.users_group_id == member_id)\
808 .one()
808 .one()
809
809
810 except Exception:
810 except Exception:
811 log.exception('Updated permission failed: org_exc:')
811 log.exception('Updated permission failed: org_exc:')
812 msg = M(self, 'perm_new_member_type', state)
812 msg = M(self, 'perm_new_member_type', state)
813 raise formencode.Invalid(
813 raise formencode.Invalid(
814 msg, value, state, error_dict={
814 msg, value, state, error_dict={
815 'perm_new_member_name': msg}
815 'perm_new_member_name': msg}
816 )
816 )
817 return value
817 return value
818 return _validator
818 return _validator
819
819
820
820
821 def ValidSettings():
821 def ValidSettings():
822 class _validator(formencode.validators.FancyValidator):
822 class _validator(formencode.validators.FancyValidator):
823 def _to_python(self, value, state):
823 def _to_python(self, value, state):
824 # settings form for users that are not admin
824 # settings form for users that are not admin
825 # can't edit certain parameters, it's extra backup if they mangle
825 # can't edit certain parameters, it's extra backup if they mangle
826 # with forms
826 # with forms
827
827
828 forbidden_params = [
828 forbidden_params = [
829 'user', 'repo_type', 'repo_enable_locking',
829 'user', 'repo_type', 'repo_enable_locking',
830 'repo_enable_downloads', 'repo_enable_statistics'
830 'repo_enable_downloads', 'repo_enable_statistics'
831 ]
831 ]
832
832
833 for param in forbidden_params:
833 for param in forbidden_params:
834 if param in value:
834 if param in value:
835 del value[param]
835 del value[param]
836 return value
836 return value
837
837
838 def validate_python(self, value, state):
838 def validate_python(self, value, state):
839 pass
839 pass
840 return _validator
840 return _validator
841
841
842
842
843 def ValidPath():
843 def ValidPath():
844 class _validator(formencode.validators.FancyValidator):
844 class _validator(formencode.validators.FancyValidator):
845 messages = {
845 messages = {
846 'invalid_path': _(u'This is not a valid path')
846 'invalid_path': _(u'This is not a valid path')
847 }
847 }
848
848
849 def validate_python(self, value, state):
849 def validate_python(self, value, state):
850 if not os.path.isdir(value):
850 if not os.path.isdir(value):
851 msg = M(self, 'invalid_path', state)
851 msg = M(self, 'invalid_path', state)
852 raise formencode.Invalid(
852 raise formencode.Invalid(
853 msg, value, state, error_dict={'paths_root_path': msg}
853 msg, value, state, error_dict={'paths_root_path': msg}
854 )
854 )
855 return _validator
855 return _validator
856
856
857
857
858 def UniqSystemEmail(old_data={}):
858 def UniqSystemEmail(old_data={}):
859 class _validator(formencode.validators.FancyValidator):
859 class _validator(formencode.validators.FancyValidator):
860 messages = {
860 messages = {
861 'email_taken': _(u'This e-mail address is already taken')
861 'email_taken': _(u'This e-mail address is already taken')
862 }
862 }
863
863
864 def _to_python(self, value, state):
864 def _to_python(self, value, state):
865 return value.lower()
865 return value.lower()
866
866
867 def validate_python(self, value, state):
867 def validate_python(self, value, state):
868 if (old_data.get('email') or '').lower() != value:
868 if (old_data.get('email') or '').lower() != value:
869 user = User.get_by_email(value, case_insensitive=True)
869 user = User.get_by_email(value, case_insensitive=True)
870 if user:
870 if user:
871 msg = M(self, 'email_taken', state)
871 msg = M(self, 'email_taken', state)
872 raise formencode.Invalid(
872 raise formencode.Invalid(
873 msg, value, state, error_dict={'email': msg}
873 msg, value, state, error_dict={'email': msg}
874 )
874 )
875 return _validator
875 return _validator
876
876
877
877
878 def ValidSystemEmail():
878 def ValidSystemEmail():
879 class _validator(formencode.validators.FancyValidator):
879 class _validator(formencode.validators.FancyValidator):
880 messages = {
880 messages = {
881 'non_existing_email': _(u'e-mail "%(email)s" does not exist.')
881 'non_existing_email': _(u'e-mail "%(email)s" does not exist.')
882 }
882 }
883
883
884 def _to_python(self, value, state):
884 def _to_python(self, value, state):
885 return value.lower()
885 return value.lower()
886
886
887 def validate_python(self, value, state):
887 def validate_python(self, value, state):
888 user = User.get_by_email(value, case_insensitive=True)
888 user = User.get_by_email(value, case_insensitive=True)
889 if user is None:
889 if user is None:
890 msg = M(self, 'non_existing_email', state, email=value)
890 msg = M(self, 'non_existing_email', state, email=value)
891 raise formencode.Invalid(
891 raise formencode.Invalid(
892 msg, value, state, error_dict={'email': msg}
892 msg, value, state, error_dict={'email': msg}
893 )
893 )
894
894
895 return _validator
895 return _validator
896
896
897
897
898 def NotReviewedRevisions(repo_id):
898 def NotReviewedRevisions(repo_id):
899 class _validator(formencode.validators.FancyValidator):
899 class _validator(formencode.validators.FancyValidator):
900 messages = {
900 messages = {
901 'rev_already_reviewed':
901 'rev_already_reviewed':
902 _(u'Revisions %(revs)s are already part of pull request '
902 _(u'Revisions %(revs)s are already part of pull request '
903 u'or have set status'),
903 u'or have set status'),
904 }
904 }
905
905
906 def validate_python(self, value, state):
906 def validate_python(self, value, state):
907 # check revisions if they are not reviewed, or a part of another
907 # check revisions if they are not reviewed, or a part of another
908 # pull request
908 # pull request
909 statuses = ChangesetStatus.query()\
909 statuses = ChangesetStatus.query()\
910 .filter(ChangesetStatus.revision.in_(value))\
910 .filter(ChangesetStatus.revision.in_(value))\
911 .filter(ChangesetStatus.repo_id == repo_id)\
911 .filter(ChangesetStatus.repo_id == repo_id)\
912 .all()
912 .all()
913
913
914 errors = []
914 errors = []
915 for status in statuses:
915 for status in statuses:
916 if status.pull_request_id:
916 if status.pull_request_id:
917 errors.append(['pull_req', status.revision[:12]])
917 errors.append(['pull_req', status.revision[:12]])
918 elif status.status:
918 elif status.status:
919 errors.append(['status', status.revision[:12]])
919 errors.append(['status', status.revision[:12]])
920
920
921 if errors:
921 if errors:
922 revs = ','.join([x[1] for x in errors])
922 revs = ','.join([x[1] for x in errors])
923 msg = M(self, 'rev_already_reviewed', state, revs=revs)
923 msg = M(self, 'rev_already_reviewed', state, revs=revs)
924 raise formencode.Invalid(
924 raise formencode.Invalid(
925 msg, value, state, error_dict={'revisions': revs})
925 msg, value, state, error_dict={'revisions': revs})
926
926
927 return _validator
927 return _validator
928
928
929
929
930 def ValidIp():
930 def ValidIp():
931 class _validator(CIDR):
931 class _validator(CIDR):
932 messages = {
932 messages = {
933 'badFormat': _(u'Please enter a valid IPv4 or IpV6 address'),
933 'badFormat': _(u'Please enter a valid IPv4 or IpV6 address'),
934 'illegalBits': _(
934 'illegalBits': _(
935 u'The network size (bits) must be within the range '
935 u'The network size (bits) must be within the range '
936 u'of 0-32 (not %(bits)r)'),
936 u'of 0-32 (not %(bits)r)'),
937 }
937 }
938
938
939 # we ovveride the default to_python() call
939 # we ovveride the default to_python() call
940 def to_python(self, value, state):
940 def to_python(self, value, state):
941 v = super(_validator, self).to_python(value, state)
941 v = super(_validator, self).to_python(value, state)
942 v = v.strip()
942 v = v.strip()
943 net = ipaddress.ip_network(address=v, strict=False)
943 net = ipaddress.ip_network(address=v, strict=False)
944 return str(net)
944 return str(net)
945
945
946 def validate_python(self, value, state):
946 def validate_python(self, value, state):
947 try:
947 try:
948 addr = value.strip()
948 addr = value.strip()
949 # this raises an ValueError if address is not IpV4 or IpV6
949 # this raises an ValueError if address is not IpV4 or IpV6
950 ipaddress.ip_network(addr, strict=False)
950 ipaddress.ip_network(addr, strict=False)
951 except ValueError:
951 except ValueError:
952 raise formencode.Invalid(self.message('badFormat', state),
952 raise formencode.Invalid(self.message('badFormat', state),
953 value, state)
953 value, state)
954
954
955 return _validator
955 return _validator
956
956
957
957
958 def FieldKey():
958 def FieldKey():
959 class _validator(formencode.validators.FancyValidator):
959 class _validator(formencode.validators.FancyValidator):
960 messages = {
960 messages = {
961 'badFormat': _(
961 'badFormat': _(
962 u'Key name can only consist of letters, '
962 u'Key name can only consist of letters, '
963 u'underscore, dash or numbers'),
963 u'underscore, dash or numbers'),
964 }
964 }
965
965
966 def validate_python(self, value, state):
966 def validate_python(self, value, state):
967 if not re.match('[a-zA-Z0-9_-]+$', value):
967 if not re.match('[a-zA-Z0-9_-]+$', value):
968 raise formencode.Invalid(self.message('badFormat', state),
968 raise formencode.Invalid(self.message('badFormat', state),
969 value, state)
969 value, state)
970 return _validator
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 def ValidAuthPlugins():
973 def ValidAuthPlugins():
990 class _validator(formencode.validators.FancyValidator):
974 class _validator(formencode.validators.FancyValidator):
991 messages = {
975 messages = {
992 'import_duplicate': _(
976 'import_duplicate': _(
993 u'Plugins %(loaded)s and %(next_to_load)s '
977 u'Plugins %(loaded)s and %(next_to_load)s '
994 u'both export the same name'),
978 u'both export the same name'),
995 'missing_includeme': _(
979 'missing_includeme': _(
996 u'The plugin "%(plugin_id)s" is missing an includeme '
980 u'The plugin "%(plugin_id)s" is missing an includeme '
997 u'function.'),
981 u'function.'),
998 'import_error': _(
982 'import_error': _(
999 u'Can not load plugin "%(plugin_id)s"'),
983 u'Can not load plugin "%(plugin_id)s"'),
1000 'no_plugin': _(
984 'no_plugin': _(
1001 u'No plugin available with ID "%(plugin_id)s"'),
985 u'No plugin available with ID "%(plugin_id)s"'),
1002 }
986 }
1003
987
1004 def _to_python(self, value, state):
988 def _to_python(self, value, state):
1005 # filter empty values
989 # filter empty values
1006 return filter(lambda s: s not in [None, ''], value)
990 return filter(lambda s: s not in [None, ''], value)
1007
991
1008 def _validate_legacy_plugin_id(self, plugin_id, value, state):
992 def _validate_legacy_plugin_id(self, plugin_id, value, state):
1009 """
993 """
1010 Validates that the plugin import works. It also checks that the
994 Validates that the plugin import works. It also checks that the
1011 plugin has an includeme attribute.
995 plugin has an includeme attribute.
1012 """
996 """
1013 try:
997 try:
1014 plugin = _import_legacy_plugin(plugin_id)
998 plugin = _import_legacy_plugin(plugin_id)
1015 except Exception as e:
999 except Exception as e:
1016 log.exception(
1000 log.exception(
1017 'Exception during import of auth legacy plugin "{}"'
1001 'Exception during import of auth legacy plugin "{}"'
1018 .format(plugin_id))
1002 .format(plugin_id))
1019 msg = M(self, 'import_error', plugin_id=plugin_id)
1003 msg = M(self, 'import_error', plugin_id=plugin_id)
1020 raise formencode.Invalid(msg, value, state)
1004 raise formencode.Invalid(msg, value, state)
1021
1005
1022 if not hasattr(plugin, 'includeme'):
1006 if not hasattr(plugin, 'includeme'):
1023 msg = M(self, 'missing_includeme', plugin_id=plugin_id)
1007 msg = M(self, 'missing_includeme', plugin_id=plugin_id)
1024 raise formencode.Invalid(msg, value, state)
1008 raise formencode.Invalid(msg, value, state)
1025
1009
1026 return plugin
1010 return plugin
1027
1011
1028 def _validate_plugin_id(self, plugin_id, value, state):
1012 def _validate_plugin_id(self, plugin_id, value, state):
1029 """
1013 """
1030 Plugins are already imported during app start up. Therefore this
1014 Plugins are already imported during app start up. Therefore this
1031 validation only retrieves the plugin from the plugin registry and
1015 validation only retrieves the plugin from the plugin registry and
1032 if it returns something not None everything is OK.
1016 if it returns something not None everything is OK.
1033 """
1017 """
1034 plugin = loadplugin(plugin_id)
1018 plugin = loadplugin(plugin_id)
1035
1019
1036 if plugin is None:
1020 if plugin is None:
1037 msg = M(self, 'no_plugin', plugin_id=plugin_id)
1021 msg = M(self, 'no_plugin', plugin_id=plugin_id)
1038 raise formencode.Invalid(msg, value, state)
1022 raise formencode.Invalid(msg, value, state)
1039
1023
1040 return plugin
1024 return plugin
1041
1025
1042 def validate_python(self, value, state):
1026 def validate_python(self, value, state):
1043 unique_names = {}
1027 unique_names = {}
1044 for plugin_id in value:
1028 for plugin_id in value:
1045
1029
1046 # Validate legacy or normal plugin.
1030 # Validate legacy or normal plugin.
1047 if plugin_id.startswith(legacy_plugin_prefix):
1031 if plugin_id.startswith(legacy_plugin_prefix):
1048 plugin = self._validate_legacy_plugin_id(
1032 plugin = self._validate_legacy_plugin_id(
1049 plugin_id, value, state)
1033 plugin_id, value, state)
1050 else:
1034 else:
1051 plugin = self._validate_plugin_id(plugin_id, value, state)
1035 plugin = self._validate_plugin_id(plugin_id, value, state)
1052
1036
1053 # Only allow unique plugin names.
1037 # Only allow unique plugin names.
1054 if plugin.name in unique_names:
1038 if plugin.name in unique_names:
1055 msg = M(self, 'import_duplicate', state,
1039 msg = M(self, 'import_duplicate', state,
1056 loaded=unique_names[plugin.name],
1040 loaded=unique_names[plugin.name],
1057 next_to_load=plugin)
1041 next_to_load=plugin)
1058 raise formencode.Invalid(msg, value, state)
1042 raise formencode.Invalid(msg, value, state)
1059 unique_names[plugin.name] = plugin
1043 unique_names[plugin.name] = plugin
1060
1044
1061 return _validator
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 def ValidPattern():
1048 def ValidPattern():
1085
1049
1086 class _Validator(formencode.validators.FancyValidator):
1050 class _Validator(formencode.validators.FancyValidator):
1087
1051
1088 def _to_python(self, value, state):
1052 def _to_python(self, value, state):
1089 patterns = []
1053 patterns = []
1090
1054
1091 prefix = 'new_pattern'
1055 prefix = 'new_pattern'
1092 for name, v in value.iteritems():
1056 for name, v in value.iteritems():
1093 pattern_name = '_'.join((prefix, 'pattern'))
1057 pattern_name = '_'.join((prefix, 'pattern'))
1094 if name.startswith(pattern_name):
1058 if name.startswith(pattern_name):
1095 new_item_id = name[len(pattern_name)+1:]
1059 new_item_id = name[len(pattern_name)+1:]
1096
1060
1097 def _field(name):
1061 def _field(name):
1098 return '%s_%s_%s' % (prefix, name, new_item_id)
1062 return '%s_%s_%s' % (prefix, name, new_item_id)
1099
1063
1100 values = {
1064 values = {
1101 'issuetracker_pat': value.get(_field('pattern')),
1065 'issuetracker_pat': value.get(_field('pattern')),
1102 'issuetracker_pat': value.get(_field('pattern')),
1066 'issuetracker_pat': value.get(_field('pattern')),
1103 'issuetracker_url': value.get(_field('url')),
1067 'issuetracker_url': value.get(_field('url')),
1104 'issuetracker_pref': value.get(_field('prefix')),
1068 'issuetracker_pref': value.get(_field('prefix')),
1105 'issuetracker_desc': value.get(_field('description'))
1069 'issuetracker_desc': value.get(_field('description'))
1106 }
1070 }
1107 new_uid = md5(values['issuetracker_pat'])
1071 new_uid = md5(values['issuetracker_pat'])
1108
1072
1109 has_required_fields = (
1073 has_required_fields = (
1110 values['issuetracker_pat']
1074 values['issuetracker_pat']
1111 and values['issuetracker_url'])
1075 and values['issuetracker_url'])
1112
1076
1113 if has_required_fields:
1077 if has_required_fields:
1114 settings = [
1078 settings = [
1115 ('_'.join((key, new_uid)), values[key], 'unicode')
1079 ('_'.join((key, new_uid)), values[key], 'unicode')
1116 for key in values]
1080 for key in values]
1117 patterns.append(settings)
1081 patterns.append(settings)
1118
1082
1119 value['patterns'] = patterns
1083 value['patterns'] = patterns
1120 delete_patterns = value.get('uid') or []
1084 delete_patterns = value.get('uid') or []
1121 if not isinstance(delete_patterns, (list, tuple)):
1085 if not isinstance(delete_patterns, (list, tuple)):
1122 delete_patterns = [delete_patterns]
1086 delete_patterns = [delete_patterns]
1123 value['delete_patterns'] = delete_patterns
1087 value['delete_patterns'] = delete_patterns
1124 return value
1088 return value
1125 return _Validator
1089 return _Validator
@@ -1,136 +1,140 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.html"/>
2 <%inherit file="/base/base.html"/>
3
3
4 <%def name="title()">
4 <%def name="title()">
5 ${_('Edit Gist')} &middot; ${c.gist.gist_access_id}
5 ${_('Edit Gist')} &middot; ${c.gist.gist_access_id}
6 %if c.rhodecode_name:
6 %if c.rhodecode_name:
7 &middot; ${h.branding(c.rhodecode_name)}
7 &middot; ${h.branding(c.rhodecode_name)}
8 %endif
8 %endif
9 </%def>
9 </%def>
10
10
11 <%def name="breadcrumbs_links()">
11 <%def name="breadcrumbs_links()">
12 ${_('Edit Gist')} &middot; ${c.gist.gist_access_id}
12 ${_('Edit Gist')} &middot; ${c.gist.gist_access_id}
13 </%def>
13 </%def>
14
14
15 <%def name="menu_bar_nav()">
15 <%def name="menu_bar_nav()">
16 ${self.menu_items(active='gists')}
16 ${self.menu_items(active='gists')}
17 </%def>
17 </%def>
18
18
19 <%def name="main()">
19 <%def name="main()">
20 <div class="box">
20 <div class="box">
21 <!-- box / title -->
21 <!-- box / title -->
22 <div class="title">
22 <div class="title">
23 ${self.breadcrumbs()}
23 ${self.breadcrumbs()}
24 </div>
24 </div>
25
25
26 <div class="table">
26 <div class="table">
27 <div id="edit_error" class="flash_msg" style="display:none;">
27 <div id="edit_error" class="flash_msg" style="display:none;">
28 <div class="alert alert-warning">
28 <div class="alert alert-warning">
29 ${h.literal(_('Gist was updated since you started editing. Copy your changes and click %(here)s to reload the new version.')
29 ${h.literal(_('Gist was updated since you started editing. Copy your changes and click %(here)s to reload the new version.')
30 % {'here': h.link_to('here',h.url('edit_gist', gist_id=c.gist.gist_access_id))})}
30 % {'here': h.link_to('here',h.url('edit_gist', gist_id=c.gist.gist_access_id))})}
31 </div>
31 </div>
32 </div>
32 </div>
33
33
34 <div id="files_data">
34 <div id="files_data">
35 ${h.secure_form(h.url('edit_gist', gist_id=c.gist.gist_access_id), method='post', id='eform')}
35 ${h.secure_form(h.url('edit_gist', gist_id=c.gist.gist_access_id), method='post', id='eform')}
36 <div>
36 <div>
37 <input type="hidden" value="${c.file_last_commit.raw_id}" name="parent_hash">
37 <input type="hidden" value="${c.file_last_commit.raw_id}" name="parent_hash">
38 <textarea id="description" name="description"
38 <textarea id="description" name="description"
39 placeholder="${_('Gist description ...')}">${c.gist.gist_description}</textarea>
39 placeholder="${_('Gist description ...')}">${c.gist.gist_description}</textarea>
40 <div>
40 <div>
41 <span class="gist-gravatar">
41 <span class="gist-gravatar">
42 ${self.gravatar(h.email_or_none(c.rhodecode_user.full_contact), 30)}
42 ${self.gravatar(h.email_or_none(c.rhodecode_user.full_contact), 30)}
43 </span>
43 </span>
44 <label for='lifetime'>${_('Gist lifetime')}</label>
44 <label for='lifetime'>${_('Gist lifetime')}</label>
45 ${h.dropdownmenu('lifetime', '0', c.lifetime_options)}
45 ${h.dropdownmenu('lifetime', '0', c.lifetime_options)}
46
46
47 <label for='acl_level'>${_('Gist access level')}</label>
47 <label for='gist_acl_level'>${_('Gist access level')}</label>
48 ${h.dropdownmenu('acl_level', c.gist.acl_level, c.acl_options)}
48 ${h.dropdownmenu('gist_acl_level', c.gist.acl_level, c.acl_options)}
49 </div>
49 </div>
50 </div>
50 </div>
51
51
52 ## peppercorn schema
53 <input type="hidden" name="__start__" value="nodes:sequence"/>
52 % for cnt, file in enumerate(c.files):
54 % for cnt, file in enumerate(c.files):
55 <input type="hidden" name="__start__" value="file:mapping"/>
53 <div id="codeblock" class="codeblock" >
56 <div id="codeblock" class="codeblock" >
54 <div class="code-header">
57 <div class="code-header">
55 <div class="form">
58 <div class="form">
56 <div class="fields">
59 <div class="fields">
57 <input type="hidden" value="${file.path}" name="org_files">
60 <input type="hidden" name="filename_org" value="${file.path}" >
58 <input id="filename_${h.FID('f',file.path)}" name="files" size="30" type="text" value="${file.path}">
61 <input id="filename_${h.FID('f',file.path)}" name="filename" size="30" type="text" value="${file.path}">
59 ${h.dropdownmenu('mimetypes' ,'plain',[('plain',_('plain'))],enable_filter=True, id='mimetype_'+h.FID('f',file.path))}
62 ${h.dropdownmenu('mimetype' ,'plain',[('plain',_('plain'))],enable_filter=True, id='mimetype_'+h.FID('f',file.path))}
60 </div>
63 </div>
61 </div>
64 </div>
62 </div>
65 </div>
63 <div class="editor_container">
66 <div class="editor_container">
64 <pre id="editor_pre"></pre>
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 </div>
69 </div>
67 </div>
70 </div>
71 <input type="hidden" name="__end__" />
68
72
69 ## dynamic edit box.
73 ## dynamic edit box.
70 <script type="text/javascript">
74 <script type="text/javascript">
71 $(document).ready(function(){
75 $(document).ready(function(){
72 var myCodeMirror = initCodeMirror(
76 var myCodeMirror = initCodeMirror(
73 "editor_${h.FID('f',file.path)}", '');
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 fillCodeMirrorOptions(modes_select);
80 fillCodeMirrorOptions(modes_select);
77
81
78 // try to detect the mode based on the file we edit
82 // try to detect the mode based on the file we edit
79 var mimetype = "${file.mimetype}";
83 var mimetype = "${file.mimetype}";
80 var detected_mode = detectCodeMirrorMode(
84 var detected_mode = detectCodeMirrorMode(
81 "${file.path}", mimetype);
85 "${file.path}", mimetype);
82
86
83 if(detected_mode){
87 if(detected_mode){
84 $(modes_select).select2("val", mimetype);
88 $(modes_select).select2("val", mimetype);
85 $(modes_select).change();
89 $(modes_select).change();
86 setCodeMirrorMode(myCodeMirror, detected_mode);
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 // on change of select field set mode
94 // on change of select field set mode
91 setCodeMirrorModeFromSelect(
95 setCodeMirrorModeFromSelect(
92 modes_select, filename_selector, myCodeMirror, null);
96 modes_select, filename_selector, myCodeMirror, null);
93
97
94 // on entering the new filename set mode, from given extension
98 // on entering the new filename set mode, from given extension
95 setCodeMirrorModeFromInput(
99 setCodeMirrorModeFromInput(
96 modes_select, filename_selector, myCodeMirror, null);
100 modes_select, filename_selector, myCodeMirror, null);
97 });
101 });
98 </script>
102 </script>
99
100 %endfor
103 %endfor
104 <input type="hidden" name="__end__" />
101
105
102 <div class="pull-right">
106 <div class="pull-right">
103 ${h.submit('update',_('Update Gist'),class_="btn btn-success")}
107 ${h.submit('update',_('Update Gist'),class_="btn btn-success")}
104 <a class="btn" href="${h.url('gist', gist_id=c.gist.gist_access_id)}">${_('Cancel')}</a>
108 <a class="btn" href="${h.url('gist', gist_id=c.gist.gist_access_id)}">${_('Cancel')}</a>
105 </div>
109 </div>
106 ${h.end_form()}
110 ${h.end_form()}
107 </div>
111 </div>
108 </div>
112 </div>
109
113
110 </div>
114 </div>
111 <script>
115 <script>
112 $('#update').on('click', function(e){
116 $('#update').on('click', function(e){
113 e.preventDefault();
117 e.preventDefault();
114
118
115 // check for newer version.
119 // check for newer version.
116 $.ajax({
120 $.ajax({
117 url: "${h.url('edit_gist_check_revision', gist_id=c.gist.gist_access_id)}",
121 url: "${h.url('edit_gist_check_revision', gist_id=c.gist.gist_access_id)}",
118 data: {
122 data: {
119 'revision': '${c.file_last_commit.raw_id}'
123 'revision': '${c.file_last_commit.raw_id}'
120 },
124 },
121 dataType: 'json',
125 dataType: 'json',
122 type: 'GET',
126 type: 'GET',
123 success: function(data) {
127 success: function(data) {
124 if(data.success === false){
128 if(data.success === false){
125 $('#edit_error').show();
129 $('#edit_error').show();
126 window.scrollTo(0,0);
130 window.scrollTo(0,0);
127 }
131 }
128 else{
132 else{
129 $('#eform').submit();
133 $('#eform').submit();
130 }
134 }
131 }
135 }
132 });
136 });
133 })
137 })
134
138
135 </script>
139 </script>
136 </%def>
140 </%def>
@@ -1,86 +1,86 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.html"/>
2 <%inherit file="/base/base.html"/>
3
3
4 <%def name="title()">
4 <%def name="title()">
5 ${_('New Gist')}
5 ${_('New Gist')}
6 %if c.rhodecode_name:
6 %if c.rhodecode_name:
7 &middot; ${h.branding(c.rhodecode_name)}
7 &middot; ${h.branding(c.rhodecode_name)}
8 %endif
8 %endif
9 </%def>
9 </%def>
10
10
11 <%def name="breadcrumbs_links()">
11 <%def name="breadcrumbs_links()">
12 ${_('New Gist')}
12 ${_('New Gist')}
13 </%def>
13 </%def>
14
14
15 <%def name="menu_bar_nav()">
15 <%def name="menu_bar_nav()">
16 ${self.menu_items(active='gists')}
16 ${self.menu_items(active='gists')}
17 </%def>
17 </%def>
18
18
19 <%def name="main()">
19 <%def name="main()">
20 <div class="box">
20 <div class="box">
21 <!-- box / title -->
21 <!-- box / title -->
22 <div class="title">
22 <div class="title">
23 ${self.breadcrumbs()}
23 ${self.breadcrumbs()}
24 </div>
24 </div>
25
25
26 <div class="table">
26 <div class="table">
27 <div id="files_data">
27 <div id="files_data">
28 ${h.secure_form(h.url('gists'), method='post',id='eform')}
28 ${h.secure_form(h.url('gists'), method='post',id='eform')}
29 <div>
29 <div>
30 <textarea id="description" name="description" placeholder="${_('Gist description ...')}"></textarea>
30 <textarea id="description" name="description" placeholder="${_('Gist description ...')}"></textarea>
31
31
32 <span class="gist-gravatar">
32 <span class="gist-gravatar">
33 ${self.gravatar(c.rhodecode_user.email, 30)}
33 ${self.gravatar(c.rhodecode_user.email, 30)}
34 </span>
34 </span>
35 <label for='gistid'>${_('Gist id')}</label>
35 <label for='gistid'>${_('Gist id')}</label>
36 ${h.text('gistid', placeholder=_('Auto generated'))}
36 ${h.text('gistid', placeholder=_('Auto generated'))}
37
37
38 <label for='lifetime'>${_('Gist lifetime')}</label>
38 <label for='lifetime'>${_('Gist lifetime')}</label>
39 ${h.dropdownmenu('lifetime', '', c.lifetime_options)}
39 ${h.dropdownmenu('lifetime', '', c.lifetime_options)}
40
40
41 <label for='acl_level'>${_('Gist access level')}</label>
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 </div>
44 </div>
45 <div id="codeblock" class="codeblock">
45 <div id="codeblock" class="codeblock">
46 <div class="code-header">
46 <div class="code-header">
47 <div class="form">
47 <div class="form">
48 <div class="fields">
48 <div class="fields">
49 ${h.text('filename', size=30, placeholder=_('name this file...'))}
49 ${h.text('filename', size=30, placeholder=_('name this file...'))}
50 ${h.dropdownmenu('mimetype','plain',[('plain',_('plain'))],enable_filter=True)}
50 ${h.dropdownmenu('mimetype','plain',[('plain',_('plain'))],enable_filter=True)}
51 </div>
51 </div>
52 </div>
52 </div>
53 </div>
53 </div>
54 <div id="editor_container">
54 <div id="editor_container">
55 <div id="editor_pre"></div>
55 <div id="editor_pre"></div>
56 <textarea id="editor" name="content" ></textarea>
56 <textarea id="editor" name="content" ></textarea>
57 </div>
57 </div>
58 </div>
58 </div>
59 <div class="pull-right">
59 <div class="pull-right">
60 ${h.submit('private',_('Create Private Gist'),class_="btn")}
60 ${h.submit('private',_('Create Private Gist'),class_="btn")}
61 ${h.submit('public',_('Create Public Gist'),class_="btn")}
61 ${h.submit('public',_('Create Public Gist'),class_="btn")}
62 ${h.reset('reset',_('Reset'),class_="btn")}
62 ${h.reset('reset',_('Reset'),class_="btn")}
63 </div>
63 </div>
64 ${h.end_form()}
64 ${h.end_form()}
65 </div>
65 </div>
66 </div>
66 </div>
67
67
68 </div>
68 </div>
69
69
70 <script type="text/javascript">
70 <script type="text/javascript">
71 var myCodeMirror = initCodeMirror('editor', '');
71 var myCodeMirror = initCodeMirror('editor', '');
72
72
73 var modes_select = $('#mimetype');
73 var modes_select = $('#mimetype');
74 fillCodeMirrorOptions(modes_select);
74 fillCodeMirrorOptions(modes_select);
75
75
76 var filename_selector = '#filename';
76 var filename_selector = '#filename';
77 // on change of select field set mode
77 // on change of select field set mode
78 setCodeMirrorModeFromSelect(
78 setCodeMirrorModeFromSelect(
79 modes_select, filename_selector, myCodeMirror, null);
79 modes_select, filename_selector, myCodeMirror, null);
80
80
81 // on entering the new filename set mode, from given extension
81 // on entering the new filename set mode, from given extension
82 setCodeMirrorModeFromInput(
82 setCodeMirrorModeFromInput(
83 modes_select, filename_selector, myCodeMirror, null);
83 modes_select, filename_selector, myCodeMirror, null);
84
84
85 </script>
85 </script>
86 </%def>
86 </%def>
@@ -1,363 +1,359 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import mock
21 import mock
22 import pytest
22 import pytest
23
23
24 from rhodecode.lib import helpers as h
24 from rhodecode.lib import helpers as h
25 from rhodecode.model.db import User, Gist
25 from rhodecode.model.db import User, Gist
26 from rhodecode.model.gist import GistModel
26 from rhodecode.model.gist import GistModel
27 from rhodecode.model.meta import Session
27 from rhodecode.model.meta import Session
28 from rhodecode.tests import (
28 from rhodecode.tests import (
29 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
29 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
30 TestController, assert_session_flash, url)
30 TestController, assert_session_flash, url)
31 from rhodecode.tests.utils import AssertResponse
31 from rhodecode.tests.utils import AssertResponse
32
32
33
33
34 class GistUtility(object):
34 class GistUtility(object):
35
35
36 def __init__(self):
36 def __init__(self):
37 self._gist_ids = []
37 self._gist_ids = []
38
38
39 def __call__(
39 def __call__(
40 self, f_name, content='some gist', lifetime=-1,
40 self, f_name, content='some gist', lifetime=-1,
41 description='gist-desc', gist_type='public',
41 description='gist-desc', gist_type='public',
42 acl_level=Gist.GIST_PUBLIC, owner=TEST_USER_ADMIN_LOGIN):
42 acl_level=Gist.GIST_PUBLIC, owner=TEST_USER_ADMIN_LOGIN):
43 gist_mapping = {
43 gist_mapping = {
44 f_name: {'content': content}
44 f_name: {'content': content}
45 }
45 }
46 user = User.get_by_username(owner)
46 user = User.get_by_username(owner)
47 gist = GistModel().create(
47 gist = GistModel().create(
48 description, owner=user, gist_mapping=gist_mapping,
48 description, owner=user, gist_mapping=gist_mapping,
49 gist_type=gist_type, lifetime=lifetime, gist_acl_level=acl_level)
49 gist_type=gist_type, lifetime=lifetime, gist_acl_level=acl_level)
50 Session().commit()
50 Session().commit()
51 self._gist_ids.append(gist.gist_id)
51 self._gist_ids.append(gist.gist_id)
52 return gist
52 return gist
53
53
54 def cleanup(self):
54 def cleanup(self):
55 for gist_id in self._gist_ids:
55 for gist_id in self._gist_ids:
56 gist = Gist.get(gist_id)
56 gist = Gist.get(gist_id)
57 if gist:
57 if gist:
58 Session().delete(gist)
58 Session().delete(gist)
59
59
60 Session().commit()
60 Session().commit()
61
61
62
62
63 @pytest.fixture
63 @pytest.fixture
64 def create_gist(request):
64 def create_gist(request):
65 gist_utility = GistUtility()
65 gist_utility = GistUtility()
66 request.addfinalizer(gist_utility.cleanup)
66 request.addfinalizer(gist_utility.cleanup)
67 return gist_utility
67 return gist_utility
68
68
69
69
70 class TestGistsController(TestController):
70 class TestGistsController(TestController):
71
71
72 def test_index_empty(self, create_gist):
72 def test_index_empty(self, create_gist):
73 self.log_user()
73 self.log_user()
74 response = self.app.get(url('gists'))
74 response = self.app.get(url('gists'))
75 response.mustcontain('data: [],')
75 response.mustcontain('data: [],')
76
76
77 def test_index(self, create_gist):
77 def test_index(self, create_gist):
78 self.log_user()
78 self.log_user()
79 g1 = create_gist('gist1')
79 g1 = create_gist('gist1')
80 g2 = create_gist('gist2', lifetime=1400)
80 g2 = create_gist('gist2', lifetime=1400)
81 g3 = create_gist('gist3', description='gist3-desc')
81 g3 = create_gist('gist3', description='gist3-desc')
82 g4 = create_gist('gist4', gist_type='private').gist_access_id
82 g4 = create_gist('gist4', gist_type='private').gist_access_id
83 response = self.app.get(url('gists'))
83 response = self.app.get(url('gists'))
84
84
85 response.mustcontain('gist: %s' % g1.gist_access_id)
85 response.mustcontain('gist: %s' % g1.gist_access_id)
86 response.mustcontain('gist: %s' % g2.gist_access_id)
86 response.mustcontain('gist: %s' % g2.gist_access_id)
87 response.mustcontain('gist: %s' % g3.gist_access_id)
87 response.mustcontain('gist: %s' % g3.gist_access_id)
88 response.mustcontain('gist3-desc')
88 response.mustcontain('gist3-desc')
89 response.mustcontain(no=['gist: %s' % g4])
89 response.mustcontain(no=['gist: %s' % g4])
90
90
91 # Expiration information should be visible
91 # Expiration information should be visible
92 expires_tag = '%s' % h.age_component(
92 expires_tag = '%s' % h.age_component(
93 h.time_to_datetime(g2.gist_expires))
93 h.time_to_datetime(g2.gist_expires))
94 response.mustcontain(expires_tag.replace('"', '\\"'))
94 response.mustcontain(expires_tag.replace('"', '\\"'))
95
95
96 def test_index_private_gists(self, create_gist):
96 def test_index_private_gists(self, create_gist):
97 self.log_user()
97 self.log_user()
98 gist = create_gist('gist5', gist_type='private')
98 gist = create_gist('gist5', gist_type='private')
99 response = self.app.get(url('gists', private=1))
99 response = self.app.get(url('gists', private=1))
100
100
101 # and privates
101 # and privates
102 response.mustcontain('gist: %s' % gist.gist_access_id)
102 response.mustcontain('gist: %s' % gist.gist_access_id)
103
103
104 def test_index_show_all(self, create_gist):
104 def test_index_show_all(self, create_gist):
105 self.log_user()
105 self.log_user()
106 create_gist('gist1')
106 create_gist('gist1')
107 create_gist('gist2', lifetime=1400)
107 create_gist('gist2', lifetime=1400)
108 create_gist('gist3', description='gist3-desc')
108 create_gist('gist3', description='gist3-desc')
109 create_gist('gist4', gist_type='private')
109 create_gist('gist4', gist_type='private')
110
110
111 response = self.app.get(url('gists', all=1))
111 response = self.app.get(url('gists', all=1))
112
112
113 assert len(GistModel.get_all()) == 4
113 assert len(GistModel.get_all()) == 4
114 # and privates
114 # and privates
115 for gist in GistModel.get_all():
115 for gist in GistModel.get_all():
116 response.mustcontain('gist: %s' % gist.gist_access_id)
116 response.mustcontain('gist: %s' % gist.gist_access_id)
117
117
118 def test_index_show_all_hidden_from_regular(self, create_gist):
118 def test_index_show_all_hidden_from_regular(self, create_gist):
119 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
119 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
120 create_gist('gist2', gist_type='private')
120 create_gist('gist2', gist_type='private')
121 create_gist('gist3', gist_type='private')
121 create_gist('gist3', gist_type='private')
122 create_gist('gist4', gist_type='private')
122 create_gist('gist4', gist_type='private')
123
123
124 response = self.app.get(url('gists', all=1))
124 response = self.app.get(url('gists', all=1))
125
125
126 assert len(GistModel.get_all()) == 3
126 assert len(GistModel.get_all()) == 3
127 # since we don't have access to private in this view, we
127 # since we don't have access to private in this view, we
128 # should see nothing
128 # should see nothing
129 for gist in GistModel.get_all():
129 for gist in GistModel.get_all():
130 response.mustcontain(no=['gist: %s' % gist.gist_access_id])
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 self.log_user()
133 self.log_user()
134 response = self.app.post(
134 response = self.app.post(
135 url('gists'),
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 params={'lifetime': -1,
136 params={'lifetime': -1,
145 'content': 'gist test',
137 'content': 'gist test',
146 'filename': 'foo',
138 'filename': 'foo',
147 'public': 'public',
139 'public': 'public',
148 'acl_level': Gist.ACL_LEVEL_PUBLIC,
140 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
149 'csrf_token': self.csrf_token},
141 'csrf_token': self.csrf_token},
150 status=302)
142 status=302)
151 response = response.follow()
143 response = response.follow()
152 response.mustcontain('added file: foo')
144 response.mustcontain('added file: foo')
153 response.mustcontain('gist test')
145 response.mustcontain('gist test')
154
146
155 def test_create_with_path_with_dirs(self):
147 def test_create_with_path_with_dirs(self):
156 self.log_user()
148 self.log_user()
157 response = self.app.post(url('gists'),
149 response = self.app.post(
150 url('gists'),
158 params={'lifetime': -1,
151 params={'lifetime': -1,
159 'content': 'gist test',
152 'content': 'gist test',
160 'filename': '/home/foo',
153 'filename': '/home/foo',
161 'public': 'public',
154 'public': 'public',
162 'acl_level': Gist.ACL_LEVEL_PUBLIC,
155 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
163 'csrf_token': self.csrf_token},
156 'csrf_token': self.csrf_token},
164 status=200)
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 def test_access_expired_gist(self, create_gist):
160 def test_access_expired_gist(self, create_gist):
168 self.log_user()
161 self.log_user()
169 gist = create_gist('never-see-me')
162 gist = create_gist('never-see-me')
170 gist.gist_expires = 0 # 1970
163 gist.gist_expires = 0 # 1970
171 Session().add(gist)
164 Session().add(gist)
172 Session().commit()
165 Session().commit()
173
166
174 self.app.get(url('gist', gist_id=gist.gist_access_id), status=404)
167 self.app.get(url('gist', gist_id=gist.gist_access_id), status=404)
175
168
176 def test_create_private(self):
169 def test_create_private(self):
177 self.log_user()
170 self.log_user()
178 response = self.app.post(url('gists'),
171 response = self.app.post(
172 url('gists'),
179 params={'lifetime': -1,
173 params={'lifetime': -1,
180 'content': 'private gist test',
174 'content': 'private gist test',
181 'filename': 'private-foo',
175 'filename': 'private-foo',
182 'private': 'private',
176 'private': 'private',
183 'acl_level': Gist.ACL_LEVEL_PUBLIC,
177 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
184 'csrf_token': self.csrf_token},
178 'csrf_token': self.csrf_token},
185 status=302)
179 status=302)
186 response = response.follow()
180 response = response.follow()
187 response.mustcontain('added file: private-foo<')
181 response.mustcontain('added file: private-foo<')
188 response.mustcontain('private gist test')
182 response.mustcontain('private gist test')
189 response.mustcontain('Private Gist')
183 response.mustcontain('Private Gist')
190 # Make sure private gists are not indexed by robots
184 # Make sure private gists are not indexed by robots
191 response.mustcontain(
185 response.mustcontain(
192 '<meta name="robots" content="noindex, nofollow">')
186 '<meta name="robots" content="noindex, nofollow">')
193
187
194 def test_create_private_acl_private(self):
188 def test_create_private_acl_private(self):
195 self.log_user()
189 self.log_user()
196 response = self.app.post(url('gists'),
190 response = self.app.post(
191 url('gists'),
197 params={'lifetime': -1,
192 params={'lifetime': -1,
198 'content': 'private gist test',
193 'content': 'private gist test',
199 'filename': 'private-foo',
194 'filename': 'private-foo',
200 'private': 'private',
195 'private': 'private',
201 'acl_level': Gist.ACL_LEVEL_PRIVATE,
196 'gist_acl_level': Gist.ACL_LEVEL_PRIVATE,
202 'csrf_token': self.csrf_token},
197 'csrf_token': self.csrf_token},
203 status=302)
198 status=302)
204 response = response.follow()
199 response = response.follow()
205 response.mustcontain('added file: private-foo<')
200 response.mustcontain('added file: private-foo<')
206 response.mustcontain('private gist test')
201 response.mustcontain('private gist test')
207 response.mustcontain('Private Gist')
202 response.mustcontain('Private Gist')
208 # Make sure private gists are not indexed by robots
203 # Make sure private gists are not indexed by robots
209 response.mustcontain(
204 response.mustcontain(
210 '<meta name="robots" content="noindex, nofollow">')
205 '<meta name="robots" content="noindex, nofollow">')
211
206
212 def test_create_with_description(self):
207 def test_create_with_description(self):
213 self.log_user()
208 self.log_user()
214 response = self.app.post(url('gists'),
209 response = self.app.post(
210 url('gists'),
215 params={'lifetime': -1,
211 params={'lifetime': -1,
216 'content': 'gist test',
212 'content': 'gist test',
217 'filename': 'foo-desc',
213 'filename': 'foo-desc',
218 'description': 'gist-desc',
214 'description': 'gist-desc',
219 'public': 'public',
215 'public': 'public',
220 'acl_level': Gist.ACL_LEVEL_PUBLIC,
216 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
221 'csrf_token': self.csrf_token},
217 'csrf_token': self.csrf_token},
222 status=302)
218 status=302)
223 response = response.follow()
219 response = response.follow()
224 response.mustcontain('added file: foo-desc')
220 response.mustcontain('added file: foo-desc')
225 response.mustcontain('gist test')
221 response.mustcontain('gist test')
226 response.mustcontain('gist-desc')
222 response.mustcontain('gist-desc')
227
223
228 def test_create_public_with_anonymous_access(self):
224 def test_create_public_with_anonymous_access(self):
229 self.log_user()
225 self.log_user()
230 params = {
226 params = {
231 'lifetime': -1,
227 'lifetime': -1,
232 'content': 'gist test',
228 'content': 'gist test',
233 'filename': 'foo-desc',
229 'filename': 'foo-desc',
234 'description': 'gist-desc',
230 'description': 'gist-desc',
235 'public': 'public',
231 'public': 'public',
236 'acl_level': Gist.ACL_LEVEL_PUBLIC,
232 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
237 'csrf_token': self.csrf_token
233 'csrf_token': self.csrf_token
238 }
234 }
239 response = self.app.post(url('gists'), params=params, status=302)
235 response = self.app.post(url('gists'), params=params, status=302)
240 self.logout_user()
236 self.logout_user()
241 response = response.follow()
237 response = response.follow()
242 response.mustcontain('added file: foo-desc')
238 response.mustcontain('added file: foo-desc')
243 response.mustcontain('gist test')
239 response.mustcontain('gist test')
244 response.mustcontain('gist-desc')
240 response.mustcontain('gist-desc')
245
241
246 def test_new(self):
242 def test_new(self):
247 self.log_user()
243 self.log_user()
248 self.app.get(url('new_gist'))
244 self.app.get(url('new_gist'))
249
245
250 def test_delete(self, create_gist):
246 def test_delete(self, create_gist):
251 self.log_user()
247 self.log_user()
252 gist = create_gist('delete-me')
248 gist = create_gist('delete-me')
253 response = self.app.post(
249 response = self.app.post(
254 url('gist', gist_id=gist.gist_id),
250 url('gist', gist_id=gist.gist_id),
255 params={'_method': 'delete', 'csrf_token': self.csrf_token})
251 params={'_method': 'delete', 'csrf_token': self.csrf_token})
256 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
252 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
257
253
258 def test_delete_normal_user_his_gist(self, create_gist):
254 def test_delete_normal_user_his_gist(self, create_gist):
259 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
255 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
260 gist = create_gist('delete-me', owner=TEST_USER_REGULAR_LOGIN)
256 gist = create_gist('delete-me', owner=TEST_USER_REGULAR_LOGIN)
261 response = self.app.post(
257 response = self.app.post(
262 url('gist', gist_id=gist.gist_id),
258 url('gist', gist_id=gist.gist_id),
263 params={'_method': 'delete', 'csrf_token': self.csrf_token})
259 params={'_method': 'delete', 'csrf_token': self.csrf_token})
264 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
260 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
265
261
266 def test_delete_normal_user_not_his_own_gist(self, create_gist):
262 def test_delete_normal_user_not_his_own_gist(self, create_gist):
267 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
263 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
268 gist = create_gist('delete-me')
264 gist = create_gist('delete-me')
269 self.app.post(
265 self.app.post(
270 url('gist', gist_id=gist.gist_id),
266 url('gist', gist_id=gist.gist_id),
271 params={'_method': 'delete', 'csrf_token': self.csrf_token},
267 params={'_method': 'delete', 'csrf_token': self.csrf_token},
272 status=403)
268 status=403)
273
269
274 def test_show(self, create_gist):
270 def test_show(self, create_gist):
275 gist = create_gist('gist-show-me')
271 gist = create_gist('gist-show-me')
276 response = self.app.get(url('gist', gist_id=gist.gist_access_id))
272 response = self.app.get(url('gist', gist_id=gist.gist_access_id))
277
273
278 response.mustcontain('added file: gist-show-me<')
274 response.mustcontain('added file: gist-show-me<')
279
275
280 assert_response = AssertResponse(response)
276 assert_response = AssertResponse(response)
281 assert_response.element_equals_to(
277 assert_response.element_equals_to(
282 'div.rc-user span.user',
278 'div.rc-user span.user',
283 '<span class="user"> %s</span>' % h.link_to_user('test_admin'))
279 '<span class="user"> %s</span>' % h.link_to_user('test_admin'))
284
280
285 response.mustcontain('gist-desc')
281 response.mustcontain('gist-desc')
286
282
287 def test_show_without_hg(self, create_gist):
283 def test_show_without_hg(self, create_gist):
288 with mock.patch(
284 with mock.patch(
289 'rhodecode.lib.vcs.settings.ALIASES', ['git']):
285 'rhodecode.lib.vcs.settings.ALIASES', ['git']):
290 gist = create_gist('gist-show-me-again')
286 gist = create_gist('gist-show-me-again')
291 self.app.get(url('gist', gist_id=gist.gist_access_id), status=200)
287 self.app.get(url('gist', gist_id=gist.gist_access_id), status=200)
292
288
293 def test_show_acl_private(self, create_gist):
289 def test_show_acl_private(self, create_gist):
294 gist = create_gist('gist-show-me-only-when-im-logged-in',
290 gist = create_gist('gist-show-me-only-when-im-logged-in',
295 acl_level=Gist.ACL_LEVEL_PRIVATE)
291 acl_level=Gist.ACL_LEVEL_PRIVATE)
296 self.app.get(url('gist', gist_id=gist.gist_access_id), status=404)
292 self.app.get(url('gist', gist_id=gist.gist_access_id), status=404)
297
293
298 # now we log-in we should see thi gist
294 # now we log-in we should see thi gist
299 self.log_user()
295 self.log_user()
300 response = self.app.get(url('gist', gist_id=gist.gist_access_id))
296 response = self.app.get(url('gist', gist_id=gist.gist_access_id))
301 response.mustcontain('added file: gist-show-me-only-when-im-logged-in')
297 response.mustcontain('added file: gist-show-me-only-when-im-logged-in')
302
298
303 assert_response = AssertResponse(response)
299 assert_response = AssertResponse(response)
304 assert_response.element_equals_to(
300 assert_response.element_equals_to(
305 'div.rc-user span.user',
301 'div.rc-user span.user',
306 '<span class="user"> %s</span>' % h.link_to_user('test_admin'))
302 '<span class="user"> %s</span>' % h.link_to_user('test_admin'))
307 response.mustcontain('gist-desc')
303 response.mustcontain('gist-desc')
308
304
309 def test_show_as_raw(self, create_gist):
305 def test_show_as_raw(self, create_gist):
310 gist = create_gist('gist-show-me', content='GIST CONTENT')
306 gist = create_gist('gist-show-me', content='GIST CONTENT')
311 response = self.app.get(url('formatted_gist',
307 response = self.app.get(url('formatted_gist',
312 gist_id=gist.gist_access_id, format='raw'))
308 gist_id=gist.gist_access_id, format='raw'))
313 assert response.body == 'GIST CONTENT'
309 assert response.body == 'GIST CONTENT'
314
310
315 def test_show_as_raw_individual_file(self, create_gist):
311 def test_show_as_raw_individual_file(self, create_gist):
316 gist = create_gist('gist-show-me-raw', content='GIST BODY')
312 gist = create_gist('gist-show-me-raw', content='GIST BODY')
317 response = self.app.get(url('formatted_gist_file',
313 response = self.app.get(url('formatted_gist_file',
318 gist_id=gist.gist_access_id, format='raw',
314 gist_id=gist.gist_access_id, format='raw',
319 revision='tip', f_path='gist-show-me-raw'))
315 revision='tip', f_path='gist-show-me-raw'))
320 assert response.body == 'GIST BODY'
316 assert response.body == 'GIST BODY'
321
317
322 def test_edit_page(self, create_gist):
318 def test_edit_page(self, create_gist):
323 self.log_user()
319 self.log_user()
324 gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
320 gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
325 response = self.app.get(url('edit_gist', gist_id=gist.gist_access_id))
321 response = self.app.get(url('edit_gist', gist_id=gist.gist_access_id))
326 response.mustcontain('GIST EDIT BODY')
322 response.mustcontain('GIST EDIT BODY')
327
323
328 def test_edit_page_non_logged_user(self, create_gist):
324 def test_edit_page_non_logged_user(self, create_gist):
329 gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
325 gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
330 self.app.get(url('edit_gist', gist_id=gist.gist_access_id), status=302)
326 self.app.get(url('edit_gist', gist_id=gist.gist_access_id), status=302)
331
327
332 def test_edit_normal_user_his_gist(self, create_gist):
328 def test_edit_normal_user_his_gist(self, create_gist):
333 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
329 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
334 gist = create_gist('gist-for-edit', owner=TEST_USER_REGULAR_LOGIN)
330 gist = create_gist('gist-for-edit', owner=TEST_USER_REGULAR_LOGIN)
335 self.app.get(url('edit_gist', gist_id=gist.gist_access_id, status=200))
331 self.app.get(url('edit_gist', gist_id=gist.gist_access_id, status=200))
336
332
337 def test_edit_normal_user_not_his_own_gist(self, create_gist):
333 def test_edit_normal_user_not_his_own_gist(self, create_gist):
338 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
334 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
339 gist = create_gist('delete-me')
335 gist = create_gist('delete-me')
340 self.app.get(url('edit_gist', gist_id=gist.gist_access_id), status=403)
336 self.app.get(url('edit_gist', gist_id=gist.gist_access_id), status=403)
341
337
342 def test_user_first_name_is_escaped(self, user_util, create_gist):
338 def test_user_first_name_is_escaped(self, user_util, create_gist):
343 xss_atack_string = '"><script>alert(\'First Name\')</script>'
339 xss_atack_string = '"><script>alert(\'First Name\')</script>'
344 xss_escaped_string = (
340 xss_escaped_string = (
345 '&#34;&gt;&lt;script&gt;alert(&#39;First Name&#39;)&lt;/script'
341 '&#34;&gt;&lt;script&gt;alert(&#39;First Name&#39;)&lt;/script'
346 '&gt;')
342 '&gt;')
347 password = 'test'
343 password = 'test'
348 user = user_util.create_user(
344 user = user_util.create_user(
349 firstname=xss_atack_string, password=password)
345 firstname=xss_atack_string, password=password)
350 create_gist('gist', gist_type='public', owner=user.username)
346 create_gist('gist', gist_type='public', owner=user.username)
351 response = self.app.get(url('gists'))
347 response = self.app.get(url('gists'))
352 response.mustcontain(xss_escaped_string)
348 response.mustcontain(xss_escaped_string)
353
349
354 def test_user_last_name_is_escaped(self, user_util, create_gist):
350 def test_user_last_name_is_escaped(self, user_util, create_gist):
355 xss_atack_string = '"><script>alert(\'Last Name\')</script>'
351 xss_atack_string = '"><script>alert(\'Last Name\')</script>'
356 xss_escaped_string = (
352 xss_escaped_string = (
357 '&#34;&gt;&lt;script&gt;alert(&#39;Last Name&#39;)&lt;/script&gt;')
353 '&#34;&gt;&lt;script&gt;alert(&#39;Last Name&#39;)&lt;/script&gt;')
358 password = 'test'
354 password = 'test'
359 user = user_util.create_user(
355 user = user_util.create_user(
360 lastname=xss_atack_string, password=password)
356 lastname=xss_atack_string, password=password)
361 create_gist('gist', gist_type='public', owner=user.username)
357 create_gist('gist', gist_type='public', owner=user.username)
362 response = self.app.get(url('gists'))
358 response = self.app.get(url('gists'))
363 response.mustcontain(xss_escaped_string)
359 response.mustcontain(xss_escaped_string)
@@ -1,46 +1,46 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import colander
21 import colander
22 import pytest
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 class TestGroupNameType(object):
27 class TestGroupNameType(object):
28 @pytest.mark.parametrize('given, expected', [
28 @pytest.mark.parametrize('given, expected', [
29 ('//group1/group2//', 'group1/group2'),
29 ('//group1/group2//', 'group1/group2'),
30 ('//group1///group2//', 'group1/group2'),
30 ('//group1///group2//', 'group1/group2'),
31 ('group1/group2///group3', 'group1/group2/group3')
31 ('group1/group2///group3', 'group1/group2/group3')
32 ])
32 ])
33 def test_replace_extra_slashes_cleans_up_extra_slashes(
33 def test_replace_extra_slashes_cleans_up_extra_slashes(
34 self, given, expected):
34 self, given, expected):
35 type_ = GroupNameType()
35 type_ = GroupNameType()
36 result = type_._replace_extra_slashes(given)
36 result = type_._replace_extra_slashes(given)
37 assert result == expected
37 assert result == expected
38
38
39 def test_deserialize_cleans_up_extra_slashes(self):
39 def test_deserialize_cleans_up_extra_slashes(self):
40 class TestSchema(colander.Schema):
40 class TestSchema(colander.Schema):
41 field = colander.SchemaNode(GroupNameType())
41 field = colander.SchemaNode(GroupNameType())
42
42
43 schema = TestSchema()
43 schema = TestSchema()
44 cleaned_data = schema.deserialize(
44 cleaned_data = schema.deserialize(
45 {'field': '//group1/group2///group3//'})
45 {'field': '//group1/group2///group3//'})
46 assert cleaned_data['field'] == 'group1/group2/group3'
46 assert cleaned_data['field'] == 'group1/group2/group3'
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now