##// END OF EJS Templates
chore: fixed merge conflicts
andverb -
r5546:50cf7822 merge v5.2.0 stable
parent child Browse files
Show More
@@ -0,0 +1,161 b''
1 .. _config-saml-azure-ref:
2
3
4 SAML 2.0 with Azure Entra ID
5 ----------------------------
6
7 **This plugin is available only in EE Edition.**
8
9 |RCE| supports SAML 2.0 Authentication with Azure Entra ID provider. This allows
10 users to log-in to RhodeCode via SSO mechanism of external identity provider
11 such as Azure AD. The login can be triggered either by the external IDP, or internally
12 by clicking specific authentication button on the log-in page.
13
14
15 Configuration steps
16 ^^^^^^^^^^^^^^^^^^^
17
18 To configure Duo Security SAML authentication, use the following steps:
19
20 1. From the |RCE| interface, select
21 :menuselection:`Admin --> Authentication`
22 2. Activate the `Azure Entra ID` plugin and select :guilabel:`Save`
23 3. Go to newly available menu option called `Azure Entra ID` on the left side.
24 4. Check the `enabled` check box in the plugin configuration section,
25 and fill in the required SAML information and :guilabel:`Save`, for more details,
26 see :ref:`config-saml-azure`
27
28
29 .. _config-saml-azure:
30
31
32 Example SAML Azure Entra ID configuration
33 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
34
35 Example configuration for SAML 2.0 with Azure Entra ID provider
36
37
38 Enabled
39 `True`:
40
41 .. note::
42 Enable or disable this authentication plugin.
43
44
45 Auth Cache TTL
46 `30`:
47
48 .. note::
49 Amount of seconds to cache the authentication and permissions check response call for this plugin.
50 Useful for expensive calls like LDAP to improve the performance of the system (0 means disabled).
51
52 Debug
53 `True`:
54
55 .. note::
56 Enable or disable debug mode that shows SAML errors in the RhodeCode logs.
57
58
59 Auth button name
60 `Azure Entra ID`:
61
62 .. note::
63 Alternative authentication display name. E.g AzureAuth, CorporateID etc.
64
65
66 Entity ID
67 `https://sts.windows.net/APP_ID/`:
68
69 .. note::
70 Identity Provider entity/metadata URI. Known as "Microsoft Entra Identifier"
71 E.g. https://sts.windows.net/abcd-c655-dcee-aab7-abcd/
72
73 SSO URL
74 `https://login.microsoftonline.com/APP_ID/saml2`:
75
76 .. note::
77 SSO (SingleSignOn) endpoint URL of the IdP. This can be used to initialize login, Known also as Login URL
78 E.g. https://login.microsoftonline.com/abcd-c655-dcee-aab7-abcd/saml2
79
80 SLO URL
81 `https://login.microsoftonline.com/APP_ID/saml2`:
82
83 .. note::
84 SLO (SingleLogout) endpoint URL of the IdP. , Known also as Logout URL
85 E.g. https://login.microsoftonline.com/abcd-c655-dcee-aab7-abcd/saml2
86
87 x509cert
88 `<CERTIFICATE_STRING>`:
89
90 .. note::
91 Identity provider public x509 certificate. It will be converted to single-line format without headers.
92 Download the raw base64 encoded certificate from the Identity provider and paste it here.
93
94 SAML Signature
95 `sha-256`:
96
97 .. note::
98 Type of Algorithm to use for verification of SAML signature on Identity provider side.
99
100 SAML Digest
101 `sha-256`:
102
103 .. note::
104 Type of Algorithm to use for verification of SAML digest on Identity provider side.
105
106 Service Provider Cert Dir
107 `/etc/rhodecode/conf/saml_ssl/`:
108
109 .. note::
110 Optional directory to store service provider certificate and private keys.
111 Expected certs for the SP should be stored in this folder as:
112
113 * sp.key Private Key
114 * sp.crt Public cert
115 * sp_new.crt Future Public cert
116
117 Also you can use other cert to sign the metadata of the SP using the:
118
119 * metadata.key
120 * metadata.crt
121
122 Expected NameID Format
123 `nameid-format:emailAddress`:
124
125 .. note::
126 The format that specifies how the NameID is sent to the service provider.
127
128 User ID Attribute
129 `user.email`:
130
131 .. note::
132 User ID Attribute name. This defines which attribute in SAML response will be used to link accounts via unique id.
133 Ensure this is returned from DuoSecurity for example via duo_username.
134
135 Username Attribute
136 `user.username`:
137
138 .. note::
139 Username Attribute name. This defines which attribute in SAML response will map to a username.
140
141 Email Attribute
142 `user.email`:
143
144 .. note::
145 Email Attribute name. This defines which attribute in SAML response will map to an email address.
146
147
148
149 Below is example setup from Azure Administration page that can be used with above config.
150
151 .. image:: ../images/saml-azure-service-provider-example.png
152 :alt: Azure SAML setup example
153 :scale: 50 %
154
155
156 Below is an example attribute mapping set for IDP provider required by the above config.
157
158
159 .. image:: ../images/saml-azure-attributes-example.png
160 :alt: Azure SAML setup example
161 :scale: 50 % No newline at end of file
1 NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
@@ -0,0 +1,40 b''
1 |RCE| 5.1.1 |RNS|
2 -----------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2024-07-23
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13
14
15 General
16 ^^^^^^^
17
18
19
20 Security
21 ^^^^^^^^
22
23
24
25 Performance
26 ^^^^^^^^^^^
27
28
29
30
31 Fixes
32 ^^^^^
33
34 - Fixed problems with JS static files build
35
36
37 Upgrade notes
38 ^^^^^^^^^^^^^
39
40 - RhodeCode 5.1.1 is unscheduled bugfix release to address some build issues with 5.1 images
@@ -0,0 +1,41 b''
1 |RCE| 5.1.2 |RNS|
2 -----------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2024-09-12
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13
14
15 General
16 ^^^^^^^
17
18
19
20 Security
21 ^^^^^^^^
22
23
24
25 Performance
26 ^^^^^^^^^^^
27
28
29
30
31 Fixes
32 ^^^^^
33
34 - Fixed problems with Mercurial authentication after enabling httppostargs.
35 Currently this protocol will be disabled until proper fix is in place
36
37
38 Upgrade notes
39 ^^^^^^^^^^^^^
40
41 - RhodeCode 5.1.2 is unscheduled bugfix release to address some build issues with 5.1 images
@@ -0,0 +1,55 b''
1 |RCE| 5.2.0 |RNS|
2 -----------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2024-10-09
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13 - New artifact storage engines allowing an s3 based uploads
14 - Enterprise version only: Added security tab to admin interface and possibility to whitelist specific vcs client versions. Some older versions clients have known security vulnerabilities, now you can disallow them.
15 - Enterprise version only: Implemented support for Azure SAML authentication
16
17
18 General
19 ^^^^^^^
20 - Bumped version of packaging, gunicorn, orjson, zope.interface and some other requirements
21 - Few tweaks and changes to saml plugins to allows easier setup
22 - Configs: allow json log format for gunicorn
23 - Configs: deprecated old ssh wrapper command and make the v2 the default one
24 - Make sure commit-caches propagate to parent repo groups
25 - Configs: Moved git lfs path and path of hg large files to ini file
26
27 Security
28 ^^^^^^^^
29
30
31
32 Performance
33 ^^^^^^^^^^^
34
35 - description escaper for better performance
36
37 Fixes
38 ^^^^^
39
40 - Email notifications not working properly
41 - Removed waitress as a default runner
42 - Fixed issue with branch permissions
43 - Ldap: fixed nested groups extraction logic
44 - Fixed possible db corruption in case of filesystem problems
45 - Cleanup and improvements to documentation
46 - Added Kubernetes deployment section to the documentation
47 - Added default value to celery result and broker
48 - Fixed broken backends function after python3 migration
49 - Explicitly disable mercurial web_push ssl flag to prevent from errors about ssl required
50 - VCS: fixed problems with locked repos and with branch permissions reporting
51
52 Upgrade notes
53 ^^^^^^^^^^^^^
54
55 - RhodeCode 5.2.0 is a planned major release featuring Azure SAML, whitelist for client versions, s3 artifacts backend and more!
@@ -0,0 +1,46 b''
1 # Copyright (C) 2010-2024 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 import logging
20
21 from rhodecode.apps._base import BaseAppView
22 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
23
24 log = logging.getLogger(__name__)
25
26
27 class AdminSecurityView(BaseAppView):
28
29 def load_default_context(self):
30 c = self._get_local_tmpl_context()
31 return c
32
33 @LoginRequired()
34 @HasPermissionAllDecorator('hg.admin')
35 def security(self):
36 c = self.load_default_context()
37 c.active = 'security'
38 return self._get_template_context(c)
39
40
41 @LoginRequired()
42 @HasPermissionAllDecorator('hg.admin')
43 def admin_security_modify_allowed_vcs_client_versions(self):
44 c = self.load_default_context()
45 c.active = 'security'
46 return self._get_template_context(c)
@@ -0,0 +1,269 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 import os
20 import fsspec # noqa
21 import logging
22
23 from rhodecode.lib.ext_json import json
24
25 from rhodecode.apps.file_store.utils import sha256_safe, ShardFileReader, get_uid_filename
26 from rhodecode.apps.file_store.extensions import resolve_extensions
27 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException # noqa: F401
28
29 log = logging.getLogger(__name__)
30
31
32 class BaseShard:
33
34 metadata_suffix: str = '.metadata'
35 storage_type: str = ''
36 fs = None
37
38 @property
39 def storage_medium(self):
40 if not self.storage_type:
41 raise ValueError('No storage type set for this shard storage_type=""')
42 return getattr(self, self.storage_type)
43
44 def __contains__(self, key):
45 full_path = self.store_path(key)
46 return self.fs.exists(full_path)
47
48 def metadata_convert(self, uid_filename, metadata):
49 return metadata
50
51 def get_metadata_filename(self, uid_filename) -> tuple[str, str]:
52 metadata_file: str = f'{uid_filename}{self.metadata_suffix}'
53 return metadata_file, self.store_path(metadata_file)
54
55 def get_metadata(self, uid_filename, ignore_missing=False) -> dict:
56 _metadata_file, metadata_file_path = self.get_metadata_filename(uid_filename)
57 if ignore_missing and not self.fs.exists(metadata_file_path):
58 return {}
59
60 with self.fs.open(metadata_file_path, 'rb') as f:
61 metadata = json.loads(f.read())
62
63 metadata = self.metadata_convert(uid_filename, metadata)
64 return metadata
65
66 def _store(self, key: str, uid_key: str, value_reader, max_filesize: int | None = None, metadata: dict | None = None, **kwargs):
67 raise NotImplementedError
68
69 def store(self, key: str, uid_key: str, value_reader, max_filesize: int | None = None, metadata: dict | None = None, **kwargs):
70 return self._store(key, uid_key, value_reader, max_filesize, metadata, **kwargs)
71
72 def _fetch(self, key, presigned_url_expires: int = 0):
73 raise NotImplementedError
74
75 def fetch(self, key, **kwargs) -> tuple[ShardFileReader, dict]:
76 return self._fetch(key)
77
78 def _delete(self, key):
79 if key not in self:
80 log.exception(f'requested key={key} not found in {self}')
81 raise KeyError(key)
82
83 metadata = self.get_metadata(key)
84 _metadata_file, metadata_file_path = self.get_metadata_filename(key)
85 artifact_file_path = metadata['filename_uid_path']
86 self.fs.rm(artifact_file_path)
87 self.fs.rm(metadata_file_path)
88
89 return 1
90
91 def delete(self, key):
92 raise NotImplementedError
93
94 def store_path(self, uid_filename):
95 raise NotImplementedError
96
97
98 class BaseFileStoreBackend:
99 _shards = tuple()
100 _shard_cls = BaseShard
101 _config: dict | None = None
102 _storage_path: str = ''
103
104 def __init__(self, settings, extension_groups=None):
105 self._config = settings
106 extension_groups = extension_groups or ['any']
107 self.extensions = resolve_extensions([], groups=extension_groups)
108
109 def __contains__(self, key):
110 return self.filename_exists(key)
111
112 def __repr__(self):
113 return f'<{self.__class__.__name__}(storage={self.storage_path})>'
114
115 @property
116 def storage_path(self):
117 return self._storage_path
118
119 @classmethod
120 def get_shard_index(cls, filename: str, num_shards) -> int:
121 # Generate a hash value from the filename
122 hash_value = sha256_safe(filename)
123
124 # Convert the hash value to an integer
125 hash_int = int(hash_value, 16)
126
127 # Map the hash integer to a shard number between 1 and num_shards
128 shard_number = (hash_int % num_shards)
129
130 return shard_number
131
132 @classmethod
133 def apply_counter(cls, counter: int, filename: str) -> str:
134 """
135 Apply a counter to the filename.
136
137 :param counter: The counter value to apply.
138 :param filename: The original filename.
139 :return: The modified filename with the counter.
140 """
141 name_counted = f'{counter:d}-{filename}'
142 return name_counted
143
144 def _get_shard(self, key) -> _shard_cls:
145 index = self.get_shard_index(key, len(self._shards))
146 shard = self._shards[index]
147 return shard
148
149 def get_conf(self, key, pop=False):
150 if key not in self._config:
151 raise ValueError(
152 f"No configuration key '{key}', please make sure it exists in filestore config")
153 val = self._config[key]
154 if pop:
155 del self._config[key]
156 return val
157
158 def filename_allowed(self, filename, extensions=None):
159 """Checks if a filename has an allowed extension
160
161 :param filename: base name of file
162 :param extensions: iterable of extensions (or self.extensions)
163 """
164 _, ext = os.path.splitext(filename)
165 return self.extension_allowed(ext, extensions)
166
167 def extension_allowed(self, ext, extensions=None):
168 """
169 Checks if an extension is permitted. Both e.g. ".jpg" and
170 "jpg" can be passed in. Extension lookup is case-insensitive.
171
172 :param ext: extension to check
173 :param extensions: iterable of extensions to validate against (or self.extensions)
174 """
175 def normalize_ext(_ext):
176 if _ext.startswith('.'):
177 _ext = _ext[1:]
178 return _ext.lower()
179
180 extensions = extensions or self.extensions
181 if not extensions:
182 return True
183
184 ext = normalize_ext(ext)
185
186 return ext in [normalize_ext(x) for x in extensions]
187
188 def filename_exists(self, uid_filename):
189 shard = self._get_shard(uid_filename)
190 return uid_filename in shard
191
192 def store_path(self, uid_filename):
193 """
194 Returns absolute file path of the uid_filename
195 """
196 shard = self._get_shard(uid_filename)
197 return shard.store_path(uid_filename)
198
199 def store_metadata(self, uid_filename):
200 shard = self._get_shard(uid_filename)
201 return shard.get_metadata_filename(uid_filename)
202
203 def store(self, filename, value_reader, extensions=None, metadata=None, max_filesize=None, randomized_name=True, **kwargs):
204 extensions = extensions or self.extensions
205
206 if not self.filename_allowed(filename, extensions):
207 msg = f'filename {filename} does not allow extensions {extensions}'
208 raise FileNotAllowedException(msg)
209
210 # # TODO: check why we need this setting ? it looks stupid...
211 # no_body_seek is used in stream mode importer somehow
212 # no_body_seek = kwargs.pop('no_body_seek', False)
213 # if no_body_seek:
214 # pass
215 # else:
216 # value_reader.seek(0)
217
218 uid_filename = kwargs.pop('uid_filename', None)
219 if uid_filename is None:
220 uid_filename = get_uid_filename(filename, randomized=randomized_name)
221
222 shard = self._get_shard(uid_filename)
223
224 return shard.store(filename, uid_filename, value_reader, max_filesize, metadata, **kwargs)
225
226 def import_to_store(self, value_reader, org_filename, uid_filename, metadata, **kwargs):
227 shard = self._get_shard(uid_filename)
228 max_filesize = None
229 return shard.store(org_filename, uid_filename, value_reader, max_filesize, metadata, import_mode=True)
230
231 def delete(self, uid_filename):
232 shard = self._get_shard(uid_filename)
233 return shard.delete(uid_filename)
234
235 def fetch(self, uid_filename) -> tuple[ShardFileReader, dict]:
236 shard = self._get_shard(uid_filename)
237 return shard.fetch(uid_filename)
238
239 def get_metadata(self, uid_filename, ignore_missing=False) -> dict:
240 shard = self._get_shard(uid_filename)
241 return shard.get_metadata(uid_filename, ignore_missing=ignore_missing)
242
243 def iter_keys(self):
244 for shard in self._shards:
245 if shard.fs.exists(shard.storage_medium):
246 for path, _dirs, _files in shard.fs.walk(shard.storage_medium):
247 for key_file_path in _files:
248 if key_file_path.endswith(shard.metadata_suffix):
249 yield shard, key_file_path
250
251 def iter_artifacts(self):
252 for shard, key_file in self.iter_keys():
253 json_key = f"{shard.storage_medium}/{key_file}"
254 with shard.fs.open(json_key, 'rb') as f:
255 yield shard, json.loads(f.read())['filename_uid']
256
257 def get_statistics(self):
258 total_files = 0
259 total_size = 0
260 meta = {}
261
262 for shard, key_file in self.iter_keys():
263 json_key = f"{shard.storage_medium}/{key_file}"
264 with shard.fs.open(json_key, 'rb') as f:
265 total_files += 1
266 metadata = json.loads(f.read())
267 total_size += metadata['size']
268
269 return total_files, total_size, meta
@@ -0,0 +1,183 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 import os
20 import hashlib
21 import functools
22 import time
23 import logging
24
25 from .. import config_keys
26 from ..exceptions import FileOverSizeException
27 from ..backends.base import BaseFileStoreBackend, fsspec, BaseShard, ShardFileReader
28
29 from ....lib.ext_json import json
30
31 log = logging.getLogger(__name__)
32
33
34 class FileSystemShard(BaseShard):
35 METADATA_VER = 'v2'
36 BACKEND_TYPE = config_keys.backend_filesystem
37 storage_type: str = 'directory'
38
39 def __init__(self, index, directory, directory_folder, fs, **settings):
40 self._index: int = index
41 self._directory: str = directory
42 self._directory_folder: str = directory_folder
43 self.fs = fs
44
45 @property
46 def directory(self) -> str:
47 """Cache directory final path."""
48 return os.path.join(self._directory, self._directory_folder)
49
50 def _write_file(self, full_path, iterator, max_filesize, mode='wb'):
51
52 # ensure dir exists
53 destination, _ = os.path.split(full_path)
54 if not self.fs.exists(destination):
55 self.fs.makedirs(destination)
56
57 writer = self.fs.open(full_path, mode)
58
59 digest = hashlib.sha256()
60 oversize_cleanup = False
61 with writer:
62 size = 0
63 for chunk in iterator:
64 size += len(chunk)
65 digest.update(chunk)
66 writer.write(chunk)
67
68 if max_filesize and size > max_filesize:
69 oversize_cleanup = True
70 # free up the copied file, and raise exc
71 break
72
73 writer.flush()
74 # Get the file descriptor
75 fd = writer.fileno()
76
77 # Sync the file descriptor to disk, helps with NFS cases...
78 os.fsync(fd)
79
80 if oversize_cleanup:
81 self.fs.rm(full_path)
82 raise FileOverSizeException(f'given file is over size limit ({max_filesize}): {full_path}')
83
84 sha256 = digest.hexdigest()
85 log.debug('written new artifact under %s, sha256: %s', full_path, sha256)
86 return size, sha256
87
88 def _store(self, key: str, uid_key, value_reader, max_filesize: int | None = None, metadata: dict | None = None, **kwargs):
89
90 filename = key
91 uid_filename = uid_key
92 full_path = self.store_path(uid_filename)
93
94 # STORE METADATA
95 _metadata = {
96 "version": self.METADATA_VER,
97 "store_type": self.BACKEND_TYPE,
98
99 "filename": filename,
100 "filename_uid_path": full_path,
101 "filename_uid": uid_filename,
102 "sha256": "", # NOTE: filled in by reader iteration
103
104 "store_time": time.time(),
105
106 "size": 0
107 }
108
109 if metadata:
110 if kwargs.pop('import_mode', False):
111 # in import mode, we don't need to compute metadata, we just take the old version
112 _metadata["import_mode"] = True
113 else:
114 _metadata.update(metadata)
115
116 read_iterator = iter(functools.partial(value_reader.read, 2**22), b'')
117 size, sha256 = self._write_file(full_path, read_iterator, max_filesize)
118 _metadata['size'] = size
119 _metadata['sha256'] = sha256
120
121 # after storing the artifacts, we write the metadata present
122 _metadata_file, metadata_file_path = self.get_metadata_filename(uid_key)
123
124 with self.fs.open(metadata_file_path, 'wb') as f:
125 f.write(json.dumps(_metadata))
126
127 return uid_filename, _metadata
128
129 def store_path(self, uid_filename):
130 """
131 Returns absolute file path of the uid_filename
132 """
133 return os.path.join(self._directory, self._directory_folder, uid_filename)
134
135 def _fetch(self, key, presigned_url_expires: int = 0):
136 if key not in self:
137 log.exception(f'requested key={key} not found in {self}')
138 raise KeyError(key)
139
140 metadata = self.get_metadata(key)
141
142 file_path = metadata['filename_uid_path']
143 if presigned_url_expires and presigned_url_expires > 0:
144 metadata['url'] = self.fs.url(file_path, expires=presigned_url_expires)
145
146 return ShardFileReader(self.fs.open(file_path, 'rb')), metadata
147
148 def delete(self, key):
149 return self._delete(key)
150
151
152 class FileSystemBackend(BaseFileStoreBackend):
153 shard_name: str = 'shard_{:03d}'
154 _shard_cls = FileSystemShard
155
156 def __init__(self, settings):
157 super().__init__(settings)
158
159 store_dir = self.get_conf(config_keys.filesystem_storage_path)
160 directory = os.path.expanduser(store_dir)
161
162 self._directory = directory
163 self._storage_path = directory # common path for all from BaseCache
164 self._shard_count = int(self.get_conf(config_keys.filesystem_shards, pop=True))
165 if self._shard_count < 1:
166 raise ValueError(f'{config_keys.filesystem_shards} must be 1 or more')
167
168 log.debug('Initializing %s file_store instance', self)
169 fs = fsspec.filesystem('file')
170
171 if not fs.exists(self._directory):
172 fs.makedirs(self._directory, exist_ok=True)
173
174 self._shards = tuple(
175 self._shard_cls(
176 index=num,
177 directory=directory,
178 directory_folder=self.shard_name.format(num),
179 fs=fs,
180 **settings,
181 )
182 for num in range(self._shard_count)
183 )
@@ -0,0 +1,278 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 import errno
19 import os
20 import hashlib
21 import functools
22 import time
23 import logging
24
25 from .. import config_keys
26 from ..exceptions import FileOverSizeException
27 from ..backends.base import BaseFileStoreBackend, fsspec, BaseShard, ShardFileReader
28
29 from ....lib.ext_json import json
30
31 log = logging.getLogger(__name__)
32
33
34 class LegacyFileSystemShard(BaseShard):
35 # legacy ver
36 METADATA_VER = 'v2'
37 BACKEND_TYPE = config_keys.backend_legacy_filesystem
38 storage_type: str = 'dir_struct'
39
40 # legacy suffix
41 metadata_suffix: str = '.meta'
42
43 @classmethod
44 def _sub_store_from_filename(cls, filename):
45 return filename[:2]
46
47 @classmethod
48 def apply_counter(cls, counter, filename):
49 name_counted = '%d-%s' % (counter, filename)
50 return name_counted
51
52 @classmethod
53 def safe_make_dirs(cls, dir_path):
54 if not os.path.exists(dir_path):
55 try:
56 os.makedirs(dir_path)
57 except OSError as e:
58 if e.errno != errno.EEXIST:
59 raise
60 return
61
62 @classmethod
63 def resolve_name(cls, name, directory):
64 """
65 Resolves a unique name and the correct path. If a filename
66 for that path already exists then a numeric prefix with values > 0 will be
67 added, for example test.jpg -> 1-test.jpg etc. initially file would have 0 prefix.
68
69 :param name: base name of file
70 :param directory: absolute directory path
71 """
72
73 counter = 0
74 while True:
75 name_counted = cls.apply_counter(counter, name)
76
77 # sub_store prefix to optimize disk usage, e.g some_path/ab/final_file
78 sub_store: str = cls._sub_store_from_filename(name_counted)
79 sub_store_path: str = os.path.join(directory, sub_store)
80 cls.safe_make_dirs(sub_store_path)
81
82 path = os.path.join(sub_store_path, name_counted)
83 if not os.path.exists(path):
84 return name_counted, path
85 counter += 1
86
87 def __init__(self, index, directory, directory_folder, fs, **settings):
88 self._index: int = index
89 self._directory: str = directory
90 self._directory_folder: str = directory_folder
91 self.fs = fs
92
93 @property
94 def dir_struct(self) -> str:
95 """Cache directory final path."""
96 return os.path.join(self._directory, '0-')
97
98 def _write_file(self, full_path, iterator, max_filesize, mode='wb'):
99
100 # ensure dir exists
101 destination, _ = os.path.split(full_path)
102 if not self.fs.exists(destination):
103 self.fs.makedirs(destination)
104
105 writer = self.fs.open(full_path, mode)
106
107 digest = hashlib.sha256()
108 oversize_cleanup = False
109 with writer:
110 size = 0
111 for chunk in iterator:
112 size += len(chunk)
113 digest.update(chunk)
114 writer.write(chunk)
115
116 if max_filesize and size > max_filesize:
117 # free up the copied file, and raise exc
118 oversize_cleanup = True
119 break
120
121 writer.flush()
122 # Get the file descriptor
123 fd = writer.fileno()
124
125 # Sync the file descriptor to disk, helps with NFS cases...
126 os.fsync(fd)
127
128 if oversize_cleanup:
129 self.fs.rm(full_path)
130 raise FileOverSizeException(f'given file is over size limit ({max_filesize}): {full_path}')
131
132 sha256 = digest.hexdigest()
133 log.debug('written new artifact under %s, sha256: %s', full_path, sha256)
134 return size, sha256
135
136 def _store(self, key: str, uid_key, value_reader, max_filesize: int | None = None, metadata: dict | None = None, **kwargs):
137
138 filename = key
139 uid_filename = uid_key
140
141 # NOTE:, also apply N- Counter...
142 uid_filename, full_path = self.resolve_name(uid_filename, self._directory)
143
144 # STORE METADATA
145 # TODO: make it compatible, and backward proof
146 _metadata = {
147 "version": self.METADATA_VER,
148
149 "filename": filename,
150 "filename_uid_path": full_path,
151 "filename_uid": uid_filename,
152 "sha256": "", # NOTE: filled in by reader iteration
153
154 "store_time": time.time(),
155
156 "size": 0
157 }
158 if metadata:
159 _metadata.update(metadata)
160
161 read_iterator = iter(functools.partial(value_reader.read, 2**22), b'')
162 size, sha256 = self._write_file(full_path, read_iterator, max_filesize)
163 _metadata['size'] = size
164 _metadata['sha256'] = sha256
165
166 # after storing the artifacts, we write the metadata present
167 _metadata_file, metadata_file_path = self.get_metadata_filename(uid_filename)
168
169 with self.fs.open(metadata_file_path, 'wb') as f:
170 f.write(json.dumps(_metadata))
171
172 return uid_filename, _metadata
173
174 def store_path(self, uid_filename):
175 """
176 Returns absolute file path of the uid_filename
177 """
178 prefix_dir = ''
179 if '/' in uid_filename:
180 prefix_dir, filename = uid_filename.split('/')
181 sub_store = self._sub_store_from_filename(filename)
182 else:
183 sub_store = self._sub_store_from_filename(uid_filename)
184
185 return os.path.join(self._directory, prefix_dir, sub_store, uid_filename)
186
187 def metadata_convert(self, uid_filename, metadata):
188 # NOTE: backward compat mode here... this is for file created PRE 5.2 system
189 if 'meta_ver' in metadata:
190 full_path = self.store_path(uid_filename)
191 metadata = {
192 "_converted": True,
193 "_org": metadata,
194 "version": self.METADATA_VER,
195 "store_type": self.BACKEND_TYPE,
196
197 "filename": metadata['filename'],
198 "filename_uid_path": full_path,
199 "filename_uid": uid_filename,
200 "sha256": metadata['sha256'],
201
202 "store_time": metadata['time'],
203
204 "size": metadata['size']
205 }
206 return metadata
207
208 def _fetch(self, key, presigned_url_expires: int = 0):
209 if key not in self:
210 log.exception(f'requested key={key} not found in {self}')
211 raise KeyError(key)
212
213 metadata = self.get_metadata(key)
214
215 file_path = metadata['filename_uid_path']
216 if presigned_url_expires and presigned_url_expires > 0:
217 metadata['url'] = self.fs.url(file_path, expires=presigned_url_expires)
218
219 return ShardFileReader(self.fs.open(file_path, 'rb')), metadata
220
221 def delete(self, key):
222 return self._delete(key)
223
224 def _delete(self, key):
225 if key not in self:
226 log.exception(f'requested key={key} not found in {self}')
227 raise KeyError(key)
228
229 metadata = self.get_metadata(key)
230 metadata_file, metadata_file_path = self.get_metadata_filename(key)
231 artifact_file_path = metadata['filename_uid_path']
232 self.fs.rm(artifact_file_path)
233 self.fs.rm(metadata_file_path)
234
235 def get_metadata_filename(self, uid_filename) -> tuple[str, str]:
236
237 metadata_file: str = f'{uid_filename}{self.metadata_suffix}'
238 uid_path_in_store = self.store_path(uid_filename)
239
240 metadata_file_path = f'{uid_path_in_store}{self.metadata_suffix}'
241 return metadata_file, metadata_file_path
242
243
244 class LegacyFileSystemBackend(BaseFileStoreBackend):
245 _shard_cls = LegacyFileSystemShard
246
247 def __init__(self, settings):
248 super().__init__(settings)
249
250 store_dir = self.get_conf(config_keys.legacy_filesystem_storage_path)
251 directory = os.path.expanduser(store_dir)
252
253 self._directory = directory
254 self._storage_path = directory # common path for all from BaseCache
255
256 log.debug('Initializing %s file_store instance', self)
257 fs = fsspec.filesystem('file')
258
259 if not fs.exists(self._directory):
260 fs.makedirs(self._directory, exist_ok=True)
261
262 # legacy system uses single shard
263 self._shards = tuple(
264 [
265 self._shard_cls(
266 index=0,
267 directory=directory,
268 directory_folder='',
269 fs=fs,
270 **settings,
271 )
272 ]
273 )
274
275 @classmethod
276 def get_shard_index(cls, filename: str, num_shards) -> int:
277 # legacy filesystem doesn't use shards, and always uses single shard
278 return 0
@@ -0,0 +1,184 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 import os
20 import hashlib
21 import functools
22 import time
23 import logging
24
25 from .. import config_keys
26 from ..exceptions import FileOverSizeException
27 from ..backends.base import BaseFileStoreBackend, fsspec, BaseShard, ShardFileReader
28
29 from ....lib.ext_json import json
30
31 log = logging.getLogger(__name__)
32
33
34 class S3Shard(BaseShard):
35 METADATA_VER = 'v2'
36 BACKEND_TYPE = config_keys.backend_objectstore
37 storage_type: str = 'bucket'
38
39 def __init__(self, index, bucket, bucket_folder, fs, **settings):
40 self._index: int = index
41 self._bucket_main: str = bucket
42 self._bucket_folder: str = bucket_folder
43
44 self.fs = fs
45
46 @property
47 def bucket(self) -> str:
48 """Cache bucket final path."""
49 return os.path.join(self._bucket_main, self._bucket_folder)
50
51 def _write_file(self, full_path, iterator, max_filesize, mode='wb'):
52
53 # ensure dir exists
54 destination, _ = os.path.split(full_path)
55 if not self.fs.exists(destination):
56 self.fs.makedirs(destination)
57
58 writer = self.fs.open(full_path, mode)
59
60 digest = hashlib.sha256()
61 oversize_cleanup = False
62 with writer:
63 size = 0
64 for chunk in iterator:
65 size += len(chunk)
66 digest.update(chunk)
67 writer.write(chunk)
68
69 if max_filesize and size > max_filesize:
70 oversize_cleanup = True
71 # free up the copied file, and raise exc
72 break
73
74 if oversize_cleanup:
75 self.fs.rm(full_path)
76 raise FileOverSizeException(f'given file is over size limit ({max_filesize}): {full_path}')
77
78 sha256 = digest.hexdigest()
79 log.debug('written new artifact under %s, sha256: %s', full_path, sha256)
80 return size, sha256
81
82 def _store(self, key: str, uid_key, value_reader, max_filesize: int | None = None, metadata: dict | None = None, **kwargs):
83
84 filename = key
85 uid_filename = uid_key
86 full_path = self.store_path(uid_filename)
87
88 # STORE METADATA
89 _metadata = {
90 "version": self.METADATA_VER,
91 "store_type": self.BACKEND_TYPE,
92
93 "filename": filename,
94 "filename_uid_path": full_path,
95 "filename_uid": uid_filename,
96 "sha256": "", # NOTE: filled in by reader iteration
97
98 "store_time": time.time(),
99
100 "size": 0
101 }
102
103 if metadata:
104 if kwargs.pop('import_mode', False):
105 # in import mode, we don't need to compute metadata, we just take the old version
106 _metadata["import_mode"] = True
107 else:
108 _metadata.update(metadata)
109
110 read_iterator = iter(functools.partial(value_reader.read, 2**22), b'')
111 size, sha256 = self._write_file(full_path, read_iterator, max_filesize)
112 _metadata['size'] = size
113 _metadata['sha256'] = sha256
114
115 # after storing the artifacts, we write the metadata present
116 metadata_file, metadata_file_path = self.get_metadata_filename(uid_key)
117
118 with self.fs.open(metadata_file_path, 'wb') as f:
119 f.write(json.dumps(_metadata))
120
121 return uid_filename, _metadata
122
123 def store_path(self, uid_filename):
124 """
125 Returns absolute file path of the uid_filename
126 """
127 return os.path.join(self._bucket_main, self._bucket_folder, uid_filename)
128
129 def _fetch(self, key, presigned_url_expires: int = 0):
130 if key not in self:
131 log.exception(f'requested key={key} not found in {self}')
132 raise KeyError(key)
133
134 metadata_file, metadata_file_path = self.get_metadata_filename(key)
135 with self.fs.open(metadata_file_path, 'rb') as f:
136 metadata = json.loads(f.read())
137
138 file_path = metadata['filename_uid_path']
139 if presigned_url_expires and presigned_url_expires > 0:
140 metadata['url'] = self.fs.url(file_path, expires=presigned_url_expires)
141
142 return ShardFileReader(self.fs.open(file_path, 'rb')), metadata
143
144 def delete(self, key):
145 return self._delete(key)
146
147
148 class ObjectStoreBackend(BaseFileStoreBackend):
149 shard_name: str = 'shard-{:03d}'
150 _shard_cls = S3Shard
151
152 def __init__(self, settings):
153 super().__init__(settings)
154
155 self._shard_count = int(self.get_conf(config_keys.objectstore_bucket_shards, pop=True))
156 if self._shard_count < 1:
157 raise ValueError('cache_shards must be 1 or more')
158
159 self._bucket = settings.pop(config_keys.objectstore_bucket)
160 if not self._bucket:
161 raise ValueError(f'{config_keys.objectstore_bucket} needs to have a value')
162
163 objectstore_url = self.get_conf(config_keys.objectstore_url)
164 key = settings.pop(config_keys.objectstore_key)
165 secret = settings.pop(config_keys.objectstore_secret)
166
167 self._storage_path = objectstore_url # common path for all from BaseCache
168 log.debug('Initializing %s file_store instance', self)
169 fs = fsspec.filesystem('s3', anon=False, endpoint_url=objectstore_url, key=key, secret=secret)
170
171 # init main bucket
172 if not fs.exists(self._bucket):
173 fs.mkdir(self._bucket)
174
175 self._shards = tuple(
176 self._shard_cls(
177 index=num,
178 bucket=self._bucket,
179 bucket_folder=self.shard_name.format(num),
180 fs=fs,
181 **settings,
182 )
183 for num in range(self._shard_count)
184 )
@@ -0,0 +1,128 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 import pytest
19
20 from rhodecode.apps import file_store
21 from rhodecode.apps.file_store import config_keys
22 from rhodecode.apps.file_store.backends.filesystem_legacy import LegacyFileSystemBackend
23 from rhodecode.apps.file_store.backends.filesystem import FileSystemBackend
24 from rhodecode.apps.file_store.backends.objectstore import ObjectStoreBackend
25 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
26
27 from rhodecode.apps.file_store import utils as store_utils
28 from rhodecode.apps.file_store.tests import random_binary_file, file_store_instance
29
30
31 class TestFileStoreBackends:
32
33 @pytest.mark.parametrize('backend_type, expected_instance', [
34 (config_keys.backend_legacy_filesystem, LegacyFileSystemBackend),
35 (config_keys.backend_filesystem, FileSystemBackend),
36 (config_keys.backend_objectstore, ObjectStoreBackend),
37 ])
38 def test_get_backend(self, backend_type, expected_instance, ini_settings):
39 config = ini_settings
40 config[config_keys.backend_type] = backend_type
41 f_store = store_utils.get_filestore_backend(config=config, always_init=True)
42 assert isinstance(f_store, expected_instance)
43
44 @pytest.mark.parametrize('backend_type, expected_instance', [
45 (config_keys.backend_legacy_filesystem, LegacyFileSystemBackend),
46 (config_keys.backend_filesystem, FileSystemBackend),
47 (config_keys.backend_objectstore, ObjectStoreBackend),
48 ])
49 def test_store_and_read(self, backend_type, expected_instance, ini_settings, random_binary_file):
50 filename, temp_file = random_binary_file
51 config = ini_settings
52 config[config_keys.backend_type] = backend_type
53 f_store = store_utils.get_filestore_backend(config=config, always_init=True)
54 metadata = {
55 'user_uploaded': {
56 'username': 'user1',
57 'user_id': 10,
58 'ip': '10.20.30.40'
59 }
60 }
61 store_fid, metadata = f_store.store(filename, temp_file, extra_metadata=metadata)
62 assert store_fid
63 assert metadata
64
65 # read-after write
66 reader, metadata2 = f_store.fetch(store_fid)
67 assert reader
68 assert metadata2['filename'] == filename
69
70 @pytest.mark.parametrize('backend_type, expected_instance', [
71 (config_keys.backend_legacy_filesystem, LegacyFileSystemBackend),
72 (config_keys.backend_filesystem, FileSystemBackend),
73 (config_keys.backend_objectstore, ObjectStoreBackend),
74 ])
75 def test_store_file_not_allowed(self, backend_type, expected_instance, ini_settings, random_binary_file):
76 filename, temp_file = random_binary_file
77 config = ini_settings
78 config[config_keys.backend_type] = backend_type
79 f_store = store_utils.get_filestore_backend(config=config, always_init=True)
80 with pytest.raises(FileNotAllowedException):
81 f_store.store('notallowed.exe', temp_file, extensions=['.txt'])
82
83 @pytest.mark.parametrize('backend_type, expected_instance', [
84 (config_keys.backend_legacy_filesystem, LegacyFileSystemBackend),
85 (config_keys.backend_filesystem, FileSystemBackend),
86 (config_keys.backend_objectstore, ObjectStoreBackend),
87 ])
88 def test_store_file_over_size(self, backend_type, expected_instance, ini_settings, random_binary_file):
89 filename, temp_file = random_binary_file
90 config = ini_settings
91 config[config_keys.backend_type] = backend_type
92 f_store = store_utils.get_filestore_backend(config=config, always_init=True)
93 with pytest.raises(FileOverSizeException):
94 f_store.store('toobig.exe', temp_file, extensions=['.exe'], max_filesize=124)
95
96 @pytest.mark.parametrize('backend_type, expected_instance, extra_conf', [
97 (config_keys.backend_legacy_filesystem, LegacyFileSystemBackend, {}),
98 (config_keys.backend_filesystem, FileSystemBackend, {config_keys.filesystem_storage_path: '/tmp/test-fs-store'}),
99 (config_keys.backend_objectstore, ObjectStoreBackend, {config_keys.objectstore_bucket: 'test-bucket'}),
100 ])
101 def test_store_stats_and_keys(self, backend_type, expected_instance, extra_conf, ini_settings, random_binary_file):
102 config = ini_settings
103 config[config_keys.backend_type] = backend_type
104 config.update(extra_conf)
105
106 f_store = store_utils.get_filestore_backend(config=config, always_init=True)
107
108 # purge storage before running
109 for shard, k in f_store.iter_artifacts():
110 f_store.delete(k)
111
112 for i in range(10):
113 filename, temp_file = random_binary_file
114
115 metadata = {
116 'user_uploaded': {
117 'username': 'user1',
118 'user_id': 10,
119 'ip': '10.20.30.40'
120 }
121 }
122 store_fid, metadata = f_store.store(filename, temp_file, extra_metadata=metadata)
123 assert store_fid
124 assert metadata
125
126 cnt, size, meta = f_store.get_statistics()
127 assert cnt == 10
128 assert 10 == len(list(f_store.iter_keys()))
@@ -0,0 +1,52 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 import pytest
19
20 from rhodecode.apps.file_store import utils as store_utils
21 from rhodecode.apps.file_store import config_keys
22 from rhodecode.apps.file_store.tests import generate_random_filename
23
24
25 @pytest.fixture()
26 def file_store_filesystem_instance(ini_settings):
27 config = ini_settings
28 config[config_keys.backend_type] = config_keys.backend_filesystem
29 f_store = store_utils.get_filestore_backend(config=config, always_init=True)
30 return f_store
31
32
33 class TestFileStoreFileSystemBackend:
34
35 @pytest.mark.parametrize('filename', [generate_random_filename() for _ in range(10)])
36 def test_get_shard_number(self, filename, file_store_filesystem_instance):
37 shard_number = file_store_filesystem_instance.get_shard_index(filename, len(file_store_filesystem_instance._shards))
38 # Check that the shard number is between 0 and max-shards
39 assert 0 <= shard_number <= len(file_store_filesystem_instance._shards)
40
41 @pytest.mark.parametrize('filename, expected_shard_num', [
42 ('my-name-1', 3),
43 ('my-name-2', 2),
44 ('my-name-3', 4),
45 ('my-name-4', 1),
46
47 ('rhodecode-enterprise-ce', 5),
48 ('rhodecode-enterprise-ee', 6),
49 ])
50 def test_get_shard_number_consistency(self, filename, expected_shard_num, file_store_filesystem_instance):
51 shard_number = file_store_filesystem_instance.get_shard_index(filename, len(file_store_filesystem_instance._shards))
52 assert expected_shard_num == shard_number
@@ -0,0 +1,17 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/ No newline at end of file
@@ -0,0 +1,52 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 import pytest
19
20 from rhodecode.apps.file_store import utils as store_utils
21 from rhodecode.apps.file_store import config_keys
22 from rhodecode.apps.file_store.tests import generate_random_filename
23
24
25 @pytest.fixture()
26 def file_store_legacy_instance(ini_settings):
27 config = ini_settings
28 config[config_keys.backend_type] = config_keys.backend_legacy_filesystem
29 f_store = store_utils.get_filestore_backend(config=config, always_init=True)
30 return f_store
31
32
33 class TestFileStoreLegacyBackend:
34
35 @pytest.mark.parametrize('filename', [generate_random_filename() for _ in range(10)])
36 def test_get_shard_number(self, filename, file_store_legacy_instance):
37 shard_number = file_store_legacy_instance.get_shard_index(filename, len(file_store_legacy_instance._shards))
38 # Check that the shard number is 0 for legacy filesystem store we don't use shards
39 assert shard_number == 0
40
41 @pytest.mark.parametrize('filename, expected_shard_num', [
42 ('my-name-1', 0),
43 ('my-name-2', 0),
44 ('my-name-3', 0),
45 ('my-name-4', 0),
46
47 ('rhodecode-enterprise-ce', 0),
48 ('rhodecode-enterprise-ee', 0),
49 ])
50 def test_get_shard_number_consistency(self, filename, expected_shard_num, file_store_legacy_instance):
51 shard_number = file_store_legacy_instance.get_shard_index(filename, len(file_store_legacy_instance._shards))
52 assert expected_shard_num == shard_number
@@ -0,0 +1,52 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 import pytest
19
20 from rhodecode.apps.file_store import utils as store_utils
21 from rhodecode.apps.file_store import config_keys
22 from rhodecode.apps.file_store.tests import generate_random_filename
23
24
25 @pytest.fixture()
26 def file_store_objectstore_instance(ini_settings):
27 config = ini_settings
28 config[config_keys.backend_type] = config_keys.backend_objectstore
29 f_store = store_utils.get_filestore_backend(config=config, always_init=True)
30 return f_store
31
32
33 class TestFileStoreObjectStoreBackend:
34
35 @pytest.mark.parametrize('filename', [generate_random_filename() for _ in range(10)])
36 def test_get_shard_number(self, filename, file_store_objectstore_instance):
37 shard_number = file_store_objectstore_instance.get_shard_index(filename, len(file_store_objectstore_instance._shards))
38 # Check that the shard number is between 0 and shards
39 assert 0 <= shard_number <= len(file_store_objectstore_instance._shards)
40
41 @pytest.mark.parametrize('filename, expected_shard_num', [
42 ('my-name-1', 3),
43 ('my-name-2', 2),
44 ('my-name-3', 4),
45 ('my-name-4', 1),
46
47 ('rhodecode-enterprise-ce', 5),
48 ('rhodecode-enterprise-ee', 6),
49 ])
50 def test_get_shard_number_consistency(self, filename, expected_shard_num, file_store_objectstore_instance):
51 shard_number = file_store_objectstore_instance.get_shard_index(filename, len(file_store_objectstore_instance._shards))
52 assert expected_shard_num == shard_number
@@ -0,0 +1,122 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 import sys
20 import logging
21
22 import click
23
24 from rhodecode.lib.pyramid_utils import bootstrap
25 from rhodecode.lib.ext_json import json
26 from rhodecode.model.db import FileStore
27 from rhodecode.apps.file_store import utils as store_utils
28
29 log = logging.getLogger(__name__)
30
31
32 @click.command()
33 @click.argument('ini_path', type=click.Path(exists=True))
34 @click.argument('file_uid')
35 @click.option(
36 '--source-backend-conf',
37 type=click.Path(exists=True, dir_okay=False, readable=True),
38 help='Source backend config file path in a json format'
39 )
40 @click.option(
41 '--dest-backend-conf',
42 type=click.Path(exists=True, dir_okay=False, readable=True),
43 help='Source backend config file path in a json format'
44 )
45 def main(ini_path, file_uid, source_backend_conf, dest_backend_conf):
46 return command(ini_path, file_uid, source_backend_conf, dest_backend_conf)
47
48
49 _source_settings = {}
50
51 _dest_settings = {}
52
53
54 def command(ini_path, file_uid, source_backend_conf, dest_backend_conf):
55 with bootstrap(ini_path, env={'RC_CMD_SETUP_RC': '1'}) as env:
56 migrate_func(env, file_uid, source_backend_conf, dest_backend_conf)
57
58
59 def migrate_func(env, file_uid, source_backend_conf=None, dest_backend_conf=None):
60 """
61
62 Example usage::
63
64 from rhodecode.lib.rc_commands import migrate_artifact
65 migrate_artifact._source_settings = {
66 'file_store.backend.type': 'filesystem_v1',
67 'file_store.filesystem_v1.storage_path': '/var/opt/rhodecode_data/file_store',
68 }
69 migrate_artifact._dest_settings = {
70 'file_store.backend.type': 'objectstore',
71 'file_store.objectstore.url': 'http://s3-minio:9000',
72 'file_store.objectstore.bucket': 'rhodecode-file-store',
73 'file_store.objectstore.key': 's3admin',
74 'file_store.objectstore.secret': 's3secret4',
75 'file_store.objectstore.region': 'eu-central-1',
76 }
77 for db_obj in FileStore.query().all():
78 migrate_artifact.migrate_func({}, db_obj.file_uid)
79
80 """
81
82 try:
83 from rc_ee.api.views.store_api import _store_file
84 except ImportError:
85 click.secho('ERROR: Unable to import store_api. '
86 'store_api is only available in EE edition of RhodeCode',
87 fg='red')
88 sys.exit(-1)
89
90 source_settings = _source_settings
91 if source_backend_conf:
92 source_settings = json.loads(open(source_backend_conf).read())
93 dest_settings = _dest_settings
94 if dest_backend_conf:
95 dest_settings = json.loads(open(dest_backend_conf).read())
96
97 if file_uid.isnumeric():
98 file_store_db_obj = FileStore().query() \
99 .filter(FileStore.file_store_id == file_uid) \
100 .scalar()
101 else:
102 file_store_db_obj = FileStore().query() \
103 .filter(FileStore.file_uid == file_uid) \
104 .scalar()
105 if not file_store_db_obj:
106 click.secho(f'ERROR: Unable to fetch artifact from database file_uid={file_uid}',
107 fg='red')
108 sys.exit(-1)
109
110 uid_filename = file_store_db_obj.file_uid
111 org_filename = file_store_db_obj.file_display_name
112 click.secho(f'Attempting to migrate artifact {uid_filename}, filename: {org_filename}', fg='green')
113
114 # get old version of f_store based on the data.
115
116 origin_f_store = store_utils.get_filestore_backend(source_settings, always_init=True)
117 reader, metadata = origin_f_store.fetch(uid_filename)
118
119 target_f_store = store_utils.get_filestore_backend(dest_settings, always_init=True)
120 target_f_store.import_to_store(reader, org_filename, uid_filename, metadata)
121
122 click.secho(f'Migrated artifact {uid_filename}, filename: {org_filename} into {target_f_store} storage', fg='green')
@@ -0,0 +1,17 b''
1
2 def apply_license(*args, **kwargs):
3 pass
4
5 try:
6 from rc_license.models import apply_license
7 except ImportError:
8 pass
9
10
11 def apply_license_from_file(*args, **kwargs):
12 pass
13
14 try:
15 from rc_license.models import apply_license_from_file
16 except ImportError:
17 pass
@@ -0,0 +1,50 b''
1 <%inherit file="/base/base.mako"/>
2
3 <%def name="title()">
4 ${_('Security Admin')}
5 %if c.rhodecode_name:
6 &middot; ${h.branding(c.rhodecode_name)}
7 %endif
8 </%def>
9
10 <%def name="breadcrumbs_links()"></%def>
11
12 <%def name="menu_bar_nav()">
13 ${self.menu_items(active='admin')}
14 </%def>
15
16 <%def name="menu_bar_subnav()">
17 ${self.admin_menu(active='security')}
18 </%def>
19
20 <%def name="main()">
21
22 <div class="box">
23
24 <!-- <div class="panel panel-default">-->
25 <!-- <div class="panel-heading">-->
26 <!-- <h3 class="panel-title">${_('Security Audit')}</h3>-->
27 <!-- </div>-->
28 <!-- <div class="panel-body">-->
29 <!-- <h4>${_('This feature is available in RhodeCode EE edition only. Contact {sales_email} to obtain a trial license.').format(sales_email='<a href="mailto:sales@rhodecode.com">sales@rhodecode.com</a>')|n}</h4>-->
30 <!-- <p>-->
31 <!-- ${_('You can scan your repositories for exposed secrets, passwords, etc')}-->
32 <!-- </p>-->
33 <!-- </div>-->
34 <!-- </div>-->
35
36 <div class="panel panel-default">
37 <div class="panel-heading">
38 <h3 class="panel-title">${_('Allowed client versions')}</h3>
39 </div>
40 <div class="panel-body">
41 <h4>${_('This feature is available in RhodeCode EE edition only. Contact {sales_email} to obtain a trial license.').format(sales_email='<a href="mailto:sales@rhodecode.com">sales@rhodecode.com</a>')|n}</h4>
42 <p>
43 ${_('Some outdated client versions may have security vulnerabilities. This section have rules for whitelisting versions of clients for Git, Mercurial and SVN.')}
44 </p>
45 </div>
46
47
48 </div>
49
50 </%def>
@@ -1,5 +1,5 b''
1 1 [bumpversion]
2 current_version = 5.1.2
2 current_version = 5.2.0
3 3 message = release: Bump version {current_version} to {new_version}
4 4
5 5 [bumpversion:file:rhodecode/VERSION]
@@ -1,71 +1,71 b''
1 1 syntax: glob
2 2
3 3 *.egg
4 4 *.egg-info
5 5 *.idea
6 6 *.orig
7 7 *.pyc
8 8 *.sqlite-journal
9 9 *.swp
10 10 *.tox
11 11 *.DS_Store*
12 12 rhodecode/public/js/src/components/**/*.css
13 13
14 14 syntax: regexp
15 15
16 16 #.filename
17 17 ^\.settings$
18 18 ^\.project$
19 19 ^\.pydevproject$
20 20 ^\.coverage$
21 21 ^\.cache.*$
22 22 ^\.ruff_cache.*$
23 23 ^\.rhodecode$
24 24
25 25 ^rcextensions
26 26 ^.dev
27 27 ^._dev
28 28 ^build/
29 29 ^coverage\.xml$
30 30 ^data$
31 31 ^\.eggs/
32 32 ^configs/data$
33 33 ^dev.ini$
34 34 ^acceptance_tests/dev.*\.ini$
35 35 ^dist/
36 36 ^fabfile.py
37 37 ^htmlcov
38 38 ^junit\.xml$
39 39 ^node_modules/
40 40 ^node_binaries/
41 41 ^pylint.log$
42 42 ^rcextensions/
43 43 ^result$
44 44 ^rhodecode/public/css/style.css$
45 45 ^rhodecode/public/css/style-polymer.css$
46 46 ^rhodecode/public/css/style-ipython.css$
47 47 ^rhodecode/public/js/rhodecode-components.html$
48 48 ^rhodecode/public/js/rhodecode-components.js$
49 49 ^rhodecode/public/js/scripts.js$
50 50 ^rhodecode/public/js/scripts.min.js$
51 51 ^rhodecode/public/js/src/components/root-styles.gen.html$
52 52 ^rhodecode/public/js/vendors/webcomponentsjs/
53 53 ^rhodecode\.db$
54 54 ^rhodecode\.log$
55 55 ^rhodecode_dev\.log$
56 56 ^test\.db$
57
57 ^venv/
58 58
59 59 # ac-tests
60 60 ^acceptance_tests/\.cache.*$
61 61 ^acceptance_tests/externals
62 62 ^acceptance_tests/ghostdriver.log$
63 63 ^acceptance_tests/local(_.+)?\.ini$
64 64
65 65 # docs
66 66 ^docs/_build$
67 67 ^docs/result$
68 68 ^docs-internal/_build$
69 69
70 70 # Cythonized things
71 71 ^rhodecode/.*\.(c|so)$
@@ -1,172 +1,158 b''
1 .DEFAULT_GOAL := help
2
3 # Pretty print values cf. https://misc.flogisoft.com/bash/tip_colors_and_formatting
4 RESET := \033[0m # Reset all formatting
5 GREEN := \033[0;32m # Resets before setting 16b colour (32 -- green)
6 YELLOW := \033[0;33m
7 ORANGE := \033[0;38;5;208m # Reset then set 256b colour (208 -- orange)
8 PEACH := \033[0;38;5;216m
9
10
11 ## ---------------------------------------------------------------------------------- ##
12 ## ------------------------- Help usage builder ------------------------------------- ##
13 ## ---------------------------------------------------------------------------------- ##
14 # use '# >>> Build commands' to create section
15 # use '# target: target description' to create help for target
16 .PHONY: help
17 help:
18 @echo "Usage:"
19 @cat $(MAKEFILE_LIST) | grep -E '^# >>>|^# [A-Za-z0-9_.-]+:' | sed -E 's/^# //' | awk ' \
20 BEGIN { \
21 green="\033[32m"; \
22 yellow="\033[33m"; \
23 reset="\033[0m"; \
24 section=""; \
25 } \
26 /^>>>/ { \
27 section=substr($$0, 5); \
28 printf "\n" green ">>> %s" reset "\n", section; \
29 next; \
30 } \
31 /^([A-Za-z0-9_.-]+):/ { \
32 target=$$1; \
33 gsub(/:$$/, "", target); \
34 description=substr($$0, index($$0, ":") + 2); \
35 if (description == "") { description="-"; } \
36 printf " - " yellow "%-35s" reset " %s\n", target, description; \
37 } \
38 '
39
1 40 # required for pushd to work..
2 41 SHELL = /bin/bash
3 42
4
5 # set by: PATH_TO_OUTDATED_PACKAGES=/some/path/outdated_packages.py
6 OUTDATED_PACKAGES = ${PATH_TO_OUTDATED_PACKAGES}
43 # >>> Tests commands
7 44
8 45 .PHONY: clean
9 ## Cleanup compiled and cache py files
46 # clean: Cleanup compiled and cache py files
10 47 clean:
11 48 make test-clean
12 49 find . -type f \( -iname '*.c' -o -iname '*.pyc' -o -iname '*.so' -o -iname '*.orig' \) -exec rm '{}' ';'
13 50 find . -type d -name "build" -prune -exec rm -rf '{}' ';'
14 51
15 52
16 53 .PHONY: test
17 ## run test-clean and tests
54 # test: run test-clean and tests
18 55 test:
19 56 make test-clean
20 make test-only
57 unset RC_SQLALCHEMY_DB1_URL && unset RC_DB_URL && make test-only
21 58
22 59
23 60 .PHONY: test-clean
24 ## run test-clean and tests
61 # test-clean: run test-clean and tests
25 62 test-clean:
26 63 rm -rf coverage.xml htmlcov junit.xml pylint.log result
27 64 find . -type d -name "__pycache__" -prune -exec rm -rf '{}' ';'
28 65 find . -type f \( -iname '.coverage.*' \) -exec rm '{}' ';'
29 66
30 67
31 68 .PHONY: test-only
32 ## Run tests only without cleanup
69 # test-only: Run tests only without cleanup
33 70 test-only:
34 71 PYTHONHASHSEED=random \
35 72 py.test -x -vv -r xw -p no:sugar \
36 73 --cov-report=term-missing --cov-report=html \
37 74 --cov=rhodecode rhodecode
38 75
76 # >>> Docs commands
39 77
40 78 .PHONY: docs
41 ## build docs
79 # docs: build docs
42 80 docs:
43 81 (cd docs; docker run --rm -v $(PWD):/project --workdir=/project/docs sphinx-doc-build-rc make clean html SPHINXOPTS="-W")
44 82
45 83
46 84 .PHONY: docs-clean
47 ## Cleanup docs
85 # docs-clean: Cleanup docs
48 86 docs-clean:
49 87 (cd docs; docker run --rm -v $(PWD):/project --workdir=/project/docs sphinx-doc-build-rc make clean)
50 88
51 89
52 90 .PHONY: docs-cleanup
53 ## Cleanup docs
91 # docs-cleanup: Cleanup docs
54 92 docs-cleanup:
55 93 (cd docs; docker run --rm -v $(PWD):/project --workdir=/project/docs sphinx-doc-build-rc make cleanup)
56 94
95 # >>> Dev commands
57 96
58 97 .PHONY: web-build
59 ## Build JS packages static/js
98 # web-build: Build JS packages static/js
60 99 web-build:
61 100 rm -rf node_modules
62 101 docker run -it --rm -v $(PWD):/project --workdir=/project rhodecode/static-files-build:16 -c "npm install && /project/node_modules/.bin/grunt"
63 102 # run static file check
64 103 ./rhodecode/tests/scripts/static-file-check.sh rhodecode/public/
65 104 rm -rf node_modules
66 105
67 .PHONY: ruff-check
68 ## run a ruff analysis
69 ruff-check:
70 ruff check --ignore F401 --ignore I001 --ignore E402 --ignore E501 --ignore F841 --exclude rhodecode/lib/dbmigrate --exclude .eggs --exclude .dev .
71
72 .PHONY: pip-packages
73 ## Show outdated packages
74 pip-packages:
75 python ${OUTDATED_PACKAGES}
76
77
78 .PHONY: build
79 ## Build sdist/egg
80 build:
81 python -m build
82
83 106
84 107 .PHONY: dev-sh
85 ## make dev-sh
108 # dev-sh: make dev-sh
86 109 dev-sh:
87 110 sudo echo "deb [trusted=yes] https://apt.fury.io/rsteube/ /" | sudo tee -a "/etc/apt/sources.list.d/fury.list"
88 111 sudo apt-get update
89 112 sudo apt-get install -y zsh carapace-bin
90 113 rm -rf /home/rhodecode/.oh-my-zsh
91 114 curl https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh | sh
92 115 @echo "source <(carapace _carapace)" > /home/rhodecode/.zsrc
93 116 @echo "${RC_DEV_CMD_HELP}"
94 117 @PROMPT='%(?.%F{green}√.%F{red}?%?)%f %B%F{240}%1~%f%b %# ' zsh
95 118
96 119
97 120 .PHONY: dev-cleanup
98 ## Cleanup: pip freeze | grep -v "^-e" | grep -v "@" | xargs pip uninstall -y
121 # dev-cleanup: Cleanup: pip freeze | grep -v "^-e" | grep -v "@" | xargs pip uninstall -y
99 122 dev-cleanup:
100 123 pip freeze | grep -v "^-e" | grep -v "@" | xargs pip uninstall -y
101 124 rm -rf /tmp/*
102 125
103 126
104 127 .PHONY: dev-env
105 ## make dev-env based on the requirements files and install develop of packages
128 # dev-env: make dev-env based on the requirements files and install develop of packages
106 129 ## Cleanup: pip freeze | grep -v "^-e" | grep -v "@" | xargs pip uninstall -y
107 130 dev-env:
108 131 sudo -u root chown rhodecode:rhodecode /home/rhodecode/.cache/pip/
109 132 pip install build virtualenv
110 133 pushd ../rhodecode-vcsserver/ && make dev-env && popd
111 134 pip wheel --wheel-dir=/home/rhodecode/.cache/pip/wheels -r requirements.txt -r requirements_rc_tools.txt -r requirements_test.txt -r requirements_debug.txt
112 135 pip install --no-index --find-links=/home/rhodecode/.cache/pip/wheels -r requirements.txt -r requirements_rc_tools.txt -r requirements_test.txt -r requirements_debug.txt
113 136 pip install -e .
114 137
115 138
116 139 .PHONY: sh
117 ## shortcut for make dev-sh dev-env
140 # sh: shortcut for make dev-sh dev-env
118 141 sh:
119 142 make dev-env
120 143 make dev-sh
121 144
122 145
123 146 ## Allows changes of workers e.g make dev-srv-g workers=2
124 147 workers?=1
125 148
126 149 .PHONY: dev-srv
127 ## run gunicorn web server with reloader, use workers=N to set multiworker mode
150 # dev-srv: run gunicorn web server with reloader, use workers=N to set multiworker mode, workers=N allows changes of workers
128 151 dev-srv:
129 152 gunicorn --paste=.dev/dev.ini --bind=0.0.0.0:10020 --config=.dev/gunicorn_config.py --timeout=120 --reload --workers=$(workers)
130 153
131
132 # Default command on calling make
133 .DEFAULT_GOAL := show-help
154 .PHONY: ruff-check
155 # ruff-check: run a ruff analysis
156 ruff-check:
157 ruff check --ignore F401 --ignore I001 --ignore E402 --ignore E501 --ignore F841 --exclude rhodecode/lib/dbmigrate --exclude .eggs --exclude .dev .
134 158
135 .PHONY: show-help
136 show-help:
137 @echo "$$(tput bold)Available rules:$$(tput sgr0)"
138 @echo
139 @sed -n -e "/^## / { \
140 h; \
141 s/.*//; \
142 :doc" \
143 -e "H; \
144 n; \
145 s/^## //; \
146 t doc" \
147 -e "s/:.*//; \
148 G; \
149 s/\\n## /---/; \
150 s/\\n/ /g; \
151 p; \
152 }" ${MAKEFILE_LIST} \
153 | LC_ALL='C' sort --ignore-case \
154 | awk -F '---' \
155 -v ncol=$$(tput cols) \
156 -v indent=19 \
157 -v col_on="$$(tput setaf 6)" \
158 -v col_off="$$(tput sgr0)" \
159 '{ \
160 printf "%s%*s%s ", col_on, -indent, $$1, col_off; \
161 n = split($$2, words, " "); \
162 line_length = ncol - indent; \
163 for (i = 1; i <= n; i++) { \
164 line_length -= length(words[i]) + 1; \
165 if (line_length <= 0) { \
166 line_length = ncol - indent - length(words[i]) - 1; \
167 printf "\n%*s ", -indent, " "; \
168 } \
169 printf "%s ", words[i]; \
170 } \
171 printf "\n"; \
172 }'
@@ -1,856 +1,912 b''
1 1
2 2 ; #########################################
3 3 ; RHODECODE COMMUNITY EDITION CONFIGURATION
4 4 ; #########################################
5 5
6 6 [DEFAULT]
7 7 ; Debug flag sets all loggers to debug, and enables request tracking
8 8 debug = true
9 9
10 10 ; ########################################################################
11 11 ; EMAIL CONFIGURATION
12 12 ; These settings will be used by the RhodeCode mailing system
13 13 ; ########################################################################
14 14
15 15 ; prefix all emails subjects with given prefix, helps filtering out emails
16 16 #email_prefix = [RhodeCode]
17 17
18 18 ; email FROM address all mails will be sent
19 19 #app_email_from = rhodecode-noreply@localhost
20 20
21 21 #smtp_server = mail.server.com
22 22 #smtp_username =
23 23 #smtp_password =
24 24 #smtp_port =
25 25 #smtp_use_tls = false
26 26 #smtp_use_ssl = true
27 27
28 28 [server:main]
29 29 ; COMMON HOST/IP CONFIG, This applies mostly to develop setup,
30 30 ; Host port for gunicorn are controlled by gunicorn_conf.py
31 31 host = 127.0.0.1
32 32 port = 10020
33 33
34 34
35 35 ; ###########################
36 36 ; GUNICORN APPLICATION SERVER
37 37 ; ###########################
38 38
39 39 ; run with gunicorn --config gunicorn_conf.py --paste rhodecode.ini
40 40
41 41 ; Module to use, this setting shouldn't be changed
42 42 use = egg:gunicorn#main
43 43
44 44 ; Prefix middleware for RhodeCode.
45 45 ; recommended when using proxy setup.
46 46 ; allows to set RhodeCode under a prefix in server.
47 47 ; eg https://server.com/custom_prefix. Enable `filter-with =` option below as well.
48 48 ; And set your prefix like: `prefix = /custom_prefix`
49 49 ; be sure to also set beaker.session.cookie_path = /custom_prefix if you need
50 50 ; to make your cookies only work on prefix url
51 51 [filter:proxy-prefix]
52 52 use = egg:PasteDeploy#prefix
53 53 prefix = /
54 54
55 55 [app:main]
56 56 ; The %(here)s variable will be replaced with the absolute path of parent directory
57 57 ; of this file
58 58 ; Each option in the app:main can be override by an environmental variable
59 59 ;
60 60 ;To override an option:
61 61 ;
62 62 ;RC_<KeyName>
63 63 ;Everything should be uppercase, . and - should be replaced by _.
64 64 ;For example, if you have these configuration settings:
65 65 ;rc_cache.repo_object.backend = foo
66 66 ;can be overridden by
67 67 ;export RC_CACHE_REPO_OBJECT_BACKEND=foo
68 68
69 69 use = egg:rhodecode-enterprise-ce
70 70
71 71 ; enable proxy prefix middleware, defined above
72 72 #filter-with = proxy-prefix
73 73
74 74 ; #############
75 75 ; DEBUG OPTIONS
76 76 ; #############
77 77
78 78 pyramid.reload_templates = true
79 79
80 80 # During development the we want to have the debug toolbar enabled
81 81 pyramid.includes =
82 82 pyramid_debugtoolbar
83 83
84 84 debugtoolbar.hosts = 0.0.0.0/0
85 85 debugtoolbar.exclude_prefixes =
86 86 /css
87 87 /fonts
88 88 /images
89 89 /js
90 90
91 91 ## RHODECODE PLUGINS ##
92 92 rhodecode.includes =
93 93 rhodecode.api
94 94
95 95
96 96 # api prefix url
97 97 rhodecode.api.url = /_admin/api
98 98
99 99 ; enable debug style page
100 100 debug_style = true
101 101
102 102 ; #################
103 103 ; END DEBUG OPTIONS
104 104 ; #################
105 105
106 106 ; encryption key used to encrypt social plugin tokens,
107 107 ; remote_urls with credentials etc, if not set it defaults to
108 108 ; `beaker.session.secret`
109 109 #rhodecode.encrypted_values.secret =
110 110
111 111 ; decryption strict mode (enabled by default). It controls if decryption raises
112 112 ; `SignatureVerificationError` in case of wrong key, or damaged encryption data.
113 113 #rhodecode.encrypted_values.strict = false
114 114
115 115 ; Pick algorithm for encryption. Either fernet (more secure) or aes (default)
116 116 ; fernet is safer, and we strongly recommend switching to it.
117 117 ; Due to backward compatibility aes is used as default.
118 118 #rhodecode.encrypted_values.algorithm = fernet
119 119
120 120 ; Return gzipped responses from RhodeCode (static files/application)
121 121 gzip_responses = false
122 122
123 123 ; Auto-generate javascript routes file on startup
124 124 generate_js_files = false
125 125
126 126 ; System global default language.
127 127 ; All available languages: en (default), be, de, es, fr, it, ja, pl, pt, ru, zh
128 128 lang = en
129 129
130 130 ; Perform a full repository scan and import on each server start.
131 131 ; Settings this to true could lead to very long startup time.
132 132 startup.import_repos = false
133 133
134 134 ; URL at which the application is running. This is used for Bootstrapping
135 135 ; requests in context when no web request is available. Used in ishell, or
136 136 ; SSH calls. Set this for events to receive proper url for SSH calls.
137 137 app.base_url = http://rhodecode.local
138 138
139 139 ; Host at which the Service API is running.
140 140 app.service_api.host = http://rhodecode.local:10020
141 141
142 142 ; Secret for Service API authentication.
143 143 app.service_api.token =
144 144
145 145 ; Unique application ID. Should be a random unique string for security.
146 146 app_instance_uuid = rc-production
147 147
148 148 ; Cut off limit for large diffs (size in bytes). If overall diff size on
149 149 ; commit, or pull request exceeds this limit this diff will be displayed
150 150 ; partially. E.g 512000 == 512Kb
151 151 cut_off_limit_diff = 512000
152 152
153 153 ; Cut off limit for large files inside diffs (size in bytes). Each individual
154 154 ; file inside diff which exceeds this limit will be displayed partially.
155 155 ; E.g 128000 == 128Kb
156 156 cut_off_limit_file = 128000
157 157
158 158 ; Use cached version of vcs repositories everywhere. Recommended to be `true`
159 159 vcs_full_cache = true
160 160
161 161 ; Force https in RhodeCode, fixes https redirects, assumes it's always https.
162 162 ; Normally this is controlled by proper flags sent from http server such as Nginx or Apache
163 163 force_https = false
164 164
165 165 ; use Strict-Transport-Security headers
166 166 use_htsts = false
167 167
168 168 ; Set to true if your repos are exposed using the dumb protocol
169 169 git_update_server_info = false
170 170
171 171 ; RSS/ATOM feed options
172 172 rss_cut_off_limit = 256000
173 173 rss_items_per_page = 10
174 174 rss_include_diff = false
175 175
176 176 ; gist URL alias, used to create nicer urls for gist. This should be an
177 177 ; url that does rewrites to _admin/gists/{gistid}.
178 178 ; example: http://gist.rhodecode.org/{gistid}. Empty means use the internal
179 179 ; RhodeCode url, ie. http[s]://rhodecode.server/_admin/gists/{gistid}
180 180 gist_alias_url =
181 181
182 182 ; List of views (using glob pattern syntax) that AUTH TOKENS could be
183 183 ; used for access.
184 184 ; Adding ?auth_token=TOKEN_HASH to the url authenticates this request as if it
185 185 ; came from the the logged in user who own this authentication token.
186 186 ; Additionally @TOKEN syntax can be used to bound the view to specific
187 187 ; authentication token. Such view would be only accessible when used together
188 188 ; with this authentication token
189 189 ; list of all views can be found under `/_admin/permissions/auth_token_access`
190 190 ; The list should be "," separated and on a single line.
191 191 ; Most common views to enable:
192 192
193 193 # RepoCommitsView:repo_commit_download
194 194 # RepoCommitsView:repo_commit_patch
195 195 # RepoCommitsView:repo_commit_raw
196 196 # RepoCommitsView:repo_commit_raw@TOKEN
197 197 # RepoFilesView:repo_files_diff
198 198 # RepoFilesView:repo_archivefile
199 199 # RepoFilesView:repo_file_raw
200 200 # GistView:*
201 201 api_access_controllers_whitelist =
202 202
203 203 ; Default encoding used to convert from and to unicode
204 204 ; can be also a comma separated list of encoding in case of mixed encodings
205 205 default_encoding = UTF-8
206 206
207 207 ; instance-id prefix
208 208 ; a prefix key for this instance used for cache invalidation when running
209 209 ; multiple instances of RhodeCode, make sure it's globally unique for
210 210 ; all running RhodeCode instances. Leave empty if you don't use it
211 211 instance_id =
212 212
213 213 ; Fallback authentication plugin. Set this to a plugin ID to force the usage
214 214 ; of an authentication plugin also if it is disabled by it's settings.
215 215 ; This could be useful if you are unable to log in to the system due to broken
216 216 ; authentication settings. Then you can enable e.g. the internal RhodeCode auth
217 217 ; module to log in again and fix the settings.
218 218 ; Available builtin plugin IDs (hash is part of the ID):
219 219 ; egg:rhodecode-enterprise-ce#rhodecode
220 220 ; egg:rhodecode-enterprise-ce#pam
221 221 ; egg:rhodecode-enterprise-ce#ldap
222 222 ; egg:rhodecode-enterprise-ce#jasig_cas
223 223 ; egg:rhodecode-enterprise-ce#headers
224 224 ; egg:rhodecode-enterprise-ce#crowd
225 225
226 226 #rhodecode.auth_plugin_fallback = egg:rhodecode-enterprise-ce#rhodecode
227 227
228 228 ; Flag to control loading of legacy plugins in py:/path format
229 229 auth_plugin.import_legacy_plugins = true
230 230
231 231 ; alternative return HTTP header for failed authentication. Default HTTP
232 232 ; response is 401 HTTPUnauthorized. Currently HG clients have troubles with
233 233 ; handling that causing a series of failed authentication calls.
234 234 ; Set this variable to 403 to return HTTPForbidden, or any other HTTP code
235 235 ; This will be served instead of default 401 on bad authentication
236 236 auth_ret_code =
237 237
238 238 ; use special detection method when serving auth_ret_code, instead of serving
239 239 ; ret_code directly, use 401 initially (Which triggers credentials prompt)
240 240 ; and then serve auth_ret_code to clients
241 241 auth_ret_code_detection = false
242 242
243 243 ; locking return code. When repository is locked return this HTTP code. 2XX
244 244 ; codes don't break the transactions while 4XX codes do
245 245 lock_ret_code = 423
246 246
247 247 ; Filesystem location were repositories should be stored
248 248 repo_store.path = /var/opt/rhodecode_repo_store
249 249
250 250 ; allows to setup custom hooks in settings page
251 251 allow_custom_hooks_settings = true
252 252
253 253 ; Generated license token required for EE edition license.
254 254 ; New generated token value can be found in Admin > settings > license page.
255 255 license_token =
256 256
257 257 ; This flag hides sensitive information on the license page such as token, and license data
258 258 license.hide_license_info = false
259 259
260 ; Import EE license from this license path
261 #license.import_path = %(here)s/rhodecode_enterprise.license
262
263 ; import license 'if-missing' or 'force' (always override)
264 ; if-missing means apply license if it doesn't exist. 'force' option always overrides it
265 license.import_path_mode = if-missing
266
260 267 ; supervisor connection uri, for managing supervisor and logs.
261 268 supervisor.uri =
262 269
263 270 ; supervisord group name/id we only want this RC instance to handle
264 271 supervisor.group_id = dev
265 272
266 273 ; Display extended labs settings
267 274 labs_settings_active = true
268 275
269 276 ; Custom exception store path, defaults to TMPDIR
270 277 ; This is used to store exception from RhodeCode in shared directory
271 278 #exception_tracker.store_path =
272 279
273 280 ; Send email with exception details when it happens
274 281 #exception_tracker.send_email = false
275 282
276 283 ; Comma separated list of recipients for exception emails,
277 284 ; e.g admin@rhodecode.com,devops@rhodecode.com
278 285 ; Can be left empty, then emails will be sent to ALL super-admins
279 286 #exception_tracker.send_email_recipients =
280 287
281 288 ; optional prefix to Add to email Subject
282 289 #exception_tracker.email_prefix = [RHODECODE ERROR]
283 290
284 ; File store configuration. This is used to store and serve uploaded files
285 file_store.enabled = true
291 ; NOTE: this setting IS DEPRECATED:
292 ; file_store backend is always enabled
293 #file_store.enabled = true
286 294
295 ; NOTE: this setting IS DEPRECATED:
296 ; file_store.backend = X -> use `file_store.backend.type = filesystem_v2` instead
287 297 ; Storage backend, available options are: local
288 file_store.backend = local
298 #file_store.backend = local
289 299
300 ; NOTE: this setting IS DEPRECATED:
301 ; file_store.storage_path = X -> use `file_store.filesystem_v2.storage_path = X` instead
290 302 ; path to store the uploaded binaries and artifacts
291 file_store.storage_path = /var/opt/rhodecode_data/file_store
303 #file_store.storage_path = /var/opt/rhodecode_data/file_store
304
305 ; Artifacts file-store, is used to store comment attachments and artifacts uploads.
306 ; file_store backend type: filesystem_v1, filesystem_v2 or objectstore (s3-based) are available as options
307 ; filesystem_v1 is backwards compat with pre 5.1 storage changes
308 ; new installations should choose filesystem_v2 or objectstore (s3-based), pick filesystem when migrating from
309 ; previous installations to keep the artifacts without a need of migration
310 #file_store.backend.type = filesystem_v2
311
312 ; filesystem options...
313 #file_store.filesystem_v1.storage_path = /var/opt/rhodecode_data/artifacts_file_store
314
315 ; filesystem_v2 options...
316 #file_store.filesystem_v2.storage_path = /var/opt/rhodecode_data/artifacts_file_store
317 #file_store.filesystem_v2.shards = 8
292 318
319 ; objectstore options...
320 ; url for s3 compatible storage that allows to upload artifacts
321 ; e.g http://minio:9000
322 #file_store.backend.type = objectstore
323 #file_store.objectstore.url = http://s3-minio:9000
324
325 ; a top-level bucket to put all other shards in
326 ; objects will be stored in rhodecode-file-store/shard-N based on the bucket_shards number
327 #file_store.objectstore.bucket = rhodecode-file-store
328
329 ; number of sharded buckets to create to distribute archives across
330 ; default is 8 shards
331 #file_store.objectstore.bucket_shards = 8
332
333 ; key for s3 auth
334 #file_store.objectstore.key = s3admin
335
336 ; secret for s3 auth
337 #file_store.objectstore.secret = s3secret4
338
339 ;region for s3 storage
340 #file_store.objectstore.region = eu-central-1
293 341
294 342 ; Redis url to acquire/check generation of archives locks
295 343 archive_cache.locking.url = redis://redis:6379/1
296 344
297 345 ; Storage backend, only 'filesystem' and 'objectstore' are available now
298 346 archive_cache.backend.type = filesystem
299 347
300 348 ; url for s3 compatible storage that allows to upload artifacts
301 349 ; e.g http://minio:9000
302 350 archive_cache.objectstore.url = http://s3-minio:9000
303 351
304 352 ; key for s3 auth
305 353 archive_cache.objectstore.key = key
306 354
307 355 ; secret for s3 auth
308 356 archive_cache.objectstore.secret = secret
309 357
310 358 ;region for s3 storage
311 359 archive_cache.objectstore.region = eu-central-1
312 360
313 361 ; number of sharded buckets to create to distribute archives across
314 362 ; default is 8 shards
315 363 archive_cache.objectstore.bucket_shards = 8
316 364
317 365 ; a top-level bucket to put all other shards in
318 366 ; objects will be stored in rhodecode-archive-cache/shard-N based on the bucket_shards number
319 367 archive_cache.objectstore.bucket = rhodecode-archive-cache
320 368
321 369 ; if true, this cache will try to retry with retry_attempts=N times waiting retry_backoff time
322 370 archive_cache.objectstore.retry = false
323 371
324 372 ; number of seconds to wait for next try using retry
325 373 archive_cache.objectstore.retry_backoff = 1
326 374
327 375 ; how many tries do do a retry fetch from this backend
328 376 archive_cache.objectstore.retry_attempts = 10
329 377
330 378 ; Default is $cache_dir/archive_cache if not set
331 379 ; Generated repo archives will be cached at this location
332 380 ; and served from the cache during subsequent requests for the same archive of
333 381 ; the repository. This path is important to be shared across filesystems and with
334 382 ; RhodeCode and vcsserver
335 383 archive_cache.filesystem.store_dir = /var/opt/rhodecode_data/archive_cache
336 384
337 385 ; The limit in GB sets how much data we cache before recycling last used, defaults to 10 gb
338 386 archive_cache.filesystem.cache_size_gb = 1
339 387
340 388 ; Eviction policy used to clear out after cache_size_gb limit is reached
341 389 archive_cache.filesystem.eviction_policy = least-recently-stored
342 390
343 391 ; By default cache uses sharding technique, this specifies how many shards are there
344 392 ; default is 8 shards
345 393 archive_cache.filesystem.cache_shards = 8
346 394
347 395 ; if true, this cache will try to retry with retry_attempts=N times waiting retry_backoff time
348 396 archive_cache.filesystem.retry = false
349 397
350 398 ; number of seconds to wait for next try using retry
351 399 archive_cache.filesystem.retry_backoff = 1
352 400
353 401 ; how many tries do do a retry fetch from this backend
354 402 archive_cache.filesystem.retry_attempts = 10
355 403
356 404
357 405 ; #############
358 406 ; CELERY CONFIG
359 407 ; #############
360 408
361 409 ; manually run celery: /path/to/celery worker --task-events --beat --app rhodecode.lib.celerylib.loader --scheduler rhodecode.lib.celerylib.scheduler.RcScheduler --loglevel DEBUG --ini /path/to/rhodecode.ini
362 410
363 411 use_celery = true
364 412
365 413 ; path to store schedule database
366 414 #celerybeat-schedule.path =
367 415
368 416 ; connection url to the message broker (default redis)
369 417 celery.broker_url = redis://redis:6379/8
370 418
371 419 ; results backend to get results for (default redis)
372 420 celery.result_backend = redis://redis:6379/8
373 421
374 422 ; rabbitmq example
375 423 #celery.broker_url = amqp://rabbitmq:qweqwe@localhost:5672/rabbitmqhost
376 424
377 425 ; maximum tasks to execute before worker restart
378 426 celery.max_tasks_per_child = 20
379 427
380 428 ; tasks will never be sent to the queue, but executed locally instead.
381 429 celery.task_always_eager = false
382 430
383 431 ; #############
384 432 ; DOGPILE CACHE
385 433 ; #############
386 434
387 435 ; Default cache dir for caches. Putting this into a ramdisk can boost performance.
388 436 ; eg. /tmpfs/data_ramdisk, however this directory might require large amount of space
389 437 cache_dir = /var/opt/rhodecode_data
390 438
391 439 ; *********************************************
392 440 ; `sql_cache_short` cache for heavy SQL queries
393 441 ; Only supported backend is `memory_lru`
394 442 ; *********************************************
395 443 rc_cache.sql_cache_short.backend = dogpile.cache.rc.memory_lru
396 444 rc_cache.sql_cache_short.expiration_time = 30
397 445
398 446
399 447 ; *****************************************************
400 448 ; `cache_repo_longterm` cache for repo object instances
401 449 ; Only supported backend is `memory_lru`
402 450 ; *****************************************************
403 451 rc_cache.cache_repo_longterm.backend = dogpile.cache.rc.memory_lru
404 452 ; by default we use 30 Days, cache is still invalidated on push
405 453 rc_cache.cache_repo_longterm.expiration_time = 2592000
406 454 ; max items in LRU cache, set to smaller number to save memory, and expire last used caches
407 455 rc_cache.cache_repo_longterm.max_size = 10000
408 456
409 457
410 458 ; *********************************************
411 459 ; `cache_general` cache for general purpose use
412 460 ; for simplicity use rc.file_namespace backend,
413 461 ; for performance and scale use rc.redis
414 462 ; *********************************************
415 463 rc_cache.cache_general.backend = dogpile.cache.rc.file_namespace
416 464 rc_cache.cache_general.expiration_time = 43200
417 465 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set
418 466 #rc_cache.cache_general.arguments.filename = /tmp/cache_general_db
419 467
420 468 ; alternative `cache_general` redis backend with distributed lock
421 469 #rc_cache.cache_general.backend = dogpile.cache.rc.redis
422 470 #rc_cache.cache_general.expiration_time = 300
423 471
424 472 ; redis_expiration_time needs to be greater then expiration_time
425 473 #rc_cache.cache_general.arguments.redis_expiration_time = 7200
426 474
427 475 #rc_cache.cache_general.arguments.host = localhost
428 476 #rc_cache.cache_general.arguments.port = 6379
429 477 #rc_cache.cache_general.arguments.db = 0
430 478 #rc_cache.cache_general.arguments.socket_timeout = 30
431 479 ; more Redis options: https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends
432 480 #rc_cache.cache_general.arguments.distributed_lock = true
433 481
434 482 ; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen
435 483 #rc_cache.cache_general.arguments.lock_auto_renewal = true
436 484
437 485 ; *************************************************
438 486 ; `cache_perms` cache for permission tree, auth TTL
439 487 ; for simplicity use rc.file_namespace backend,
440 488 ; for performance and scale use rc.redis
441 489 ; *************************************************
442 490 rc_cache.cache_perms.backend = dogpile.cache.rc.file_namespace
443 491 rc_cache.cache_perms.expiration_time = 3600
444 492 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set
445 493 #rc_cache.cache_perms.arguments.filename = /tmp/cache_perms_db
446 494
447 495 ; alternative `cache_perms` redis backend with distributed lock
448 496 #rc_cache.cache_perms.backend = dogpile.cache.rc.redis
449 497 #rc_cache.cache_perms.expiration_time = 300
450 498
451 499 ; redis_expiration_time needs to be greater then expiration_time
452 500 #rc_cache.cache_perms.arguments.redis_expiration_time = 7200
453 501
454 502 #rc_cache.cache_perms.arguments.host = localhost
455 503 #rc_cache.cache_perms.arguments.port = 6379
456 504 #rc_cache.cache_perms.arguments.db = 0
457 505 #rc_cache.cache_perms.arguments.socket_timeout = 30
458 506 ; more Redis options: https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends
459 507 #rc_cache.cache_perms.arguments.distributed_lock = true
460 508
461 509 ; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen
462 510 #rc_cache.cache_perms.arguments.lock_auto_renewal = true
463 511
464 512 ; ***************************************************
465 513 ; `cache_repo` cache for file tree, Readme, RSS FEEDS
466 514 ; for simplicity use rc.file_namespace backend,
467 515 ; for performance and scale use rc.redis
468 516 ; ***************************************************
469 517 rc_cache.cache_repo.backend = dogpile.cache.rc.file_namespace
470 518 rc_cache.cache_repo.expiration_time = 2592000
471 519 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set
472 520 #rc_cache.cache_repo.arguments.filename = /tmp/cache_repo_db
473 521
474 522 ; alternative `cache_repo` redis backend with distributed lock
475 523 #rc_cache.cache_repo.backend = dogpile.cache.rc.redis
476 524 #rc_cache.cache_repo.expiration_time = 2592000
477 525
478 526 ; redis_expiration_time needs to be greater then expiration_time
479 527 #rc_cache.cache_repo.arguments.redis_expiration_time = 2678400
480 528
481 529 #rc_cache.cache_repo.arguments.host = localhost
482 530 #rc_cache.cache_repo.arguments.port = 6379
483 531 #rc_cache.cache_repo.arguments.db = 1
484 532 #rc_cache.cache_repo.arguments.socket_timeout = 30
485 533 ; more Redis options: https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends
486 534 #rc_cache.cache_repo.arguments.distributed_lock = true
487 535
488 536 ; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen
489 537 #rc_cache.cache_repo.arguments.lock_auto_renewal = true
490 538
491 539 ; ##############
492 540 ; BEAKER SESSION
493 541 ; ##############
494 542
495 543 ; beaker.session.type is type of storage options for the logged users sessions. Current allowed
496 544 ; types are file, ext:redis, ext:database, ext:memcached
497 545 ; Fastest ones are ext:redis and ext:database, DO NOT use memory type for session
498 546 #beaker.session.type = file
499 547 #beaker.session.data_dir = %(here)s/data/sessions
500 548
501 549 ; Redis based sessions
502 550 beaker.session.type = ext:redis
503 551 beaker.session.url = redis://redis:6379/2
504 552
505 553 ; DB based session, fast, and allows easy management over logged in users
506 554 #beaker.session.type = ext:database
507 555 #beaker.session.table_name = db_session
508 556 #beaker.session.sa.url = postgresql://postgres:secret@localhost/rhodecode
509 557 #beaker.session.sa.url = mysql://root:secret@127.0.0.1/rhodecode
510 558 #beaker.session.sa.pool_recycle = 3600
511 559 #beaker.session.sa.echo = false
512 560
513 561 beaker.session.key = rhodecode
514 562 beaker.session.secret = develop-rc-uytcxaz
515 563 beaker.session.lock_dir = /data_ramdisk/lock
516 564
517 565 ; Secure encrypted cookie. Requires AES and AES python libraries
518 566 ; you must disable beaker.session.secret to use this
519 567 #beaker.session.encrypt_key = key_for_encryption
520 568 #beaker.session.validate_key = validation_key
521 569
522 570 ; Sets session as invalid (also logging out user) if it haven not been
523 571 ; accessed for given amount of time in seconds
524 572 beaker.session.timeout = 2592000
525 573 beaker.session.httponly = true
526 574
527 575 ; Path to use for the cookie. Set to prefix if you use prefix middleware
528 576 #beaker.session.cookie_path = /custom_prefix
529 577
530 578 ; Set https secure cookie
531 579 beaker.session.secure = false
532 580
533 581 ; default cookie expiration time in seconds, set to `true` to set expire
534 582 ; at browser close
535 583 #beaker.session.cookie_expires = 3600
536 584
537 585 ; #############################
538 586 ; SEARCH INDEXING CONFIGURATION
539 587 ; #############################
540 588
541 589 ; Full text search indexer is available in rhodecode-tools under
542 590 ; `rhodecode-tools index` command
543 591
544 592 ; WHOOSH Backend, doesn't require additional services to run
545 593 ; it works good with few dozen repos
546 594 search.module = rhodecode.lib.index.whoosh
547 595 search.location = %(here)s/data/index
548 596
549 597 ; ####################
550 598 ; CHANNELSTREAM CONFIG
551 599 ; ####################
552 600
553 601 ; channelstream enables persistent connections and live notification
554 602 ; in the system. It's also used by the chat system
555 603
556 604 channelstream.enabled = true
557 605
558 606 ; server address for channelstream server on the backend
559 607 channelstream.server = channelstream:9800
560 608
561 609 ; location of the channelstream server from outside world
562 610 ; use ws:// for http or wss:// for https. This address needs to be handled
563 611 ; by external HTTP server such as Nginx or Apache
564 612 ; see Nginx/Apache configuration examples in our docs
565 613 channelstream.ws_url = ws://rhodecode.yourserver.com/_channelstream
566 614 channelstream.secret = ENV_GENERATED
567 615 channelstream.history.location = /var/opt/rhodecode_data/channelstream_history
568 616
569 617 ; Internal application path that Javascript uses to connect into.
570 618 ; If you use proxy-prefix the prefix should be added before /_channelstream
571 619 channelstream.proxy_path = /_channelstream
572 620
573 621
574 622 ; ##############################
575 623 ; MAIN RHODECODE DATABASE CONFIG
576 624 ; ##############################
577 625
578 626 #sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db?timeout=30
579 627 #sqlalchemy.db1.url = postgresql://postgres:qweqwe@localhost/rhodecode
580 628 #sqlalchemy.db1.url = mysql://root:qweqwe@localhost/rhodecode?charset=utf8
581 629 ; pymysql is an alternative driver for MySQL, use in case of problems with default one
582 630 #sqlalchemy.db1.url = mysql+pymysql://root:qweqwe@localhost/rhodecode
583 631
584 632 sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db?timeout=30
585 633
586 634 ; see sqlalchemy docs for other advanced settings
587 635 ; print the sql statements to output
588 636 sqlalchemy.db1.echo = false
589 637
590 638 ; recycle the connections after this amount of seconds
591 639 sqlalchemy.db1.pool_recycle = 3600
592 640
593 641 ; the number of connections to keep open inside the connection pool.
594 642 ; 0 indicates no limit
595 643 ; the general calculus with gevent is:
596 644 ; if your system allows 500 concurrent greenlets (max_connections) that all do database access,
597 645 ; then increase pool size + max overflow so that they add up to 500.
598 646 #sqlalchemy.db1.pool_size = 5
599 647
600 648 ; The number of connections to allow in connection pool "overflow", that is
601 649 ; connections that can be opened above and beyond the pool_size setting,
602 650 ; which defaults to five.
603 651 #sqlalchemy.db1.max_overflow = 10
604 652
605 653 ; Connection check ping, used to detect broken database connections
606 654 ; could be enabled to better handle cases if MySQL has gone away errors
607 655 #sqlalchemy.db1.ping_connection = true
608 656
609 657 ; ##########
610 658 ; VCS CONFIG
611 659 ; ##########
612 660 vcs.server.enable = true
613 661 vcs.server = vcsserver:10010
614 662
615 663 ; Web server connectivity protocol, responsible for web based VCS operations
616 664 ; Available protocols are:
617 665 ; `http` - use http-rpc backend (default)
618 666 vcs.server.protocol = http
619 667
620 668 ; Push/Pull operations protocol, available options are:
621 669 ; `http` - use http-rpc backend (default)
622 670 vcs.scm_app_implementation = http
623 671
624 672 ; Push/Pull operations hooks protocol, available options are:
625 673 ; `http` - use http-rpc backend (default)
626 674 ; `celery` - use celery based hooks
627 vcs.hooks.protocol = http
675 #DEPRECATED:vcs.hooks.protocol = http
676 vcs.hooks.protocol.v2 = celery
628 677
629 678 ; Host on which this instance is listening for hooks. vcsserver will call this host to pull/push hooks so it should be
630 679 ; accessible via network.
631 680 ; Use vcs.hooks.host = "*" to bind to current hostname (for Docker)
632 681 vcs.hooks.host = *
633 682
634 683 ; Start VCSServer with this instance as a subprocess, useful for development
635 684 vcs.start_server = false
636 685
637 686 ; List of enabled VCS backends, available options are:
638 687 ; `hg` - mercurial
639 688 ; `git` - git
640 689 ; `svn` - subversion
641 690 vcs.backends = hg, git, svn
642 691
643 692 ; Wait this number of seconds before killing connection to the vcsserver
644 693 vcs.connection_timeout = 3600
645 694
646 695 ; Cache flag to cache vcsserver remote calls locally
647 696 ; It uses cache_region `cache_repo`
648 697 vcs.methods.cache = true
649 698
699 ; Filesystem location where Git lfs objects should be stored
700 vcs.git.lfs.storage_location = /var/opt/rhodecode_repo_store/.cache/git_lfs_store
701
702 ; Filesystem location where Mercurial largefile objects should be stored
703 vcs.hg.largefiles.storage_location = /var/opt/rhodecode_repo_store/.cache/hg_largefiles_store
704
650 705 ; ####################################################
651 706 ; Subversion proxy support (mod_dav_svn)
652 707 ; Maps RhodeCode repo groups into SVN paths for Apache
653 708 ; ####################################################
654 709
655 710 ; Compatibility version when creating SVN repositories. Defaults to newest version when commented out.
656 711 ; Set a numeric version for your current SVN e.g 1.8, or 1.12
657 712 ; Legacy available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible, pre-1.9-compatible
658 713 #vcs.svn.compatible_version = 1.8
659 714
660 715 ; Redis connection settings for svn integrations logic
661 716 ; This connection string needs to be the same on ce and vcsserver
662 717 vcs.svn.redis_conn = redis://redis:6379/0
663 718
664 719 ; Enable SVN proxy of requests over HTTP
665 720 vcs.svn.proxy.enabled = true
666 721
667 722 ; host to connect to running SVN subsystem
668 723 vcs.svn.proxy.host = http://svn:8090
669 724
670 725 ; Enable or disable the config file generation.
671 726 svn.proxy.generate_config = true
672 727
673 728 ; Generate config file with `SVNListParentPath` set to `On`.
674 729 svn.proxy.list_parent_path = true
675 730
676 731 ; Set location and file name of generated config file.
677 732 svn.proxy.config_file_path = /etc/rhodecode/conf/svn/mod_dav_svn.conf
678 733
679 734 ; alternative mod_dav config template. This needs to be a valid mako template
680 735 ; Example template can be found in the source code:
681 736 ; rhodecode/apps/svn_support/templates/mod-dav-svn.conf.mako
682 737 #svn.proxy.config_template = ~/.rccontrol/enterprise-1/custom_svn_conf.mako
683 738
684 739 ; Used as a prefix to the `Location` block in the generated config file.
685 740 ; In most cases it should be set to `/`.
686 741 svn.proxy.location_root = /
687 742
688 743 ; Command to reload the mod dav svn configuration on change.
689 744 ; Example: `/etc/init.d/apache2 reload` or /home/USER/apache_reload.sh
690 745 ; Make sure user who runs RhodeCode process is allowed to reload Apache
691 746 #svn.proxy.reload_cmd = /etc/init.d/apache2 reload
692 747
693 748 ; If the timeout expires before the reload command finishes, the command will
694 749 ; be killed. Setting it to zero means no timeout. Defaults to 10 seconds.
695 750 #svn.proxy.reload_timeout = 10
696 751
697 752 ; ####################
698 753 ; SSH Support Settings
699 754 ; ####################
700 755
701 756 ; Defines if a custom authorized_keys file should be created and written on
702 757 ; any change user ssh keys. Setting this to false also disables possibility
703 758 ; of adding SSH keys by users from web interface. Super admins can still
704 759 ; manage SSH Keys.
705 760 ssh.generate_authorized_keyfile = true
706 761
707 762 ; Options for ssh, default is `no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding`
708 763 # ssh.authorized_keys_ssh_opts =
709 764
710 765 ; Path to the authorized_keys file where the generate entries are placed.
711 766 ; It is possible to have multiple key files specified in `sshd_config` e.g.
712 767 ; AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys_rhodecode
713 768 ssh.authorized_keys_file_path = /etc/rhodecode/conf/ssh/authorized_keys_rhodecode
714 769
715 770 ; Command to execute the SSH wrapper. The binary is available in the
716 771 ; RhodeCode installation directory.
717 772 ; legacy: /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper
718 773 ; new rewrite: /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper-v2
719 ssh.wrapper_cmd = /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper
774 #DEPRECATED: ssh.wrapper_cmd = /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper
775 ssh.wrapper_cmd.v2 = /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper-v2
720 776
721 777 ; Allow shell when executing the ssh-wrapper command
722 778 ssh.wrapper_cmd_allow_shell = false
723 779
724 780 ; Enables logging, and detailed output send back to the client during SSH
725 781 ; operations. Useful for debugging, shouldn't be used in production.
726 782 ssh.enable_debug_logging = true
727 783
728 784 ; Paths to binary executable, by default they are the names, but we can
729 785 ; override them if we want to use a custom one
730 786 ssh.executable.hg = /usr/local/bin/rhodecode_bin/vcs_bin/hg
731 787 ssh.executable.git = /usr/local/bin/rhodecode_bin/vcs_bin/git
732 788 ssh.executable.svn = /usr/local/bin/rhodecode_bin/vcs_bin/svnserve
733 789
734 790 ; Enables SSH key generator web interface. Disabling this still allows users
735 791 ; to add their own keys.
736 792 ssh.enable_ui_key_generator = true
737 793
738 794 ; Statsd client config, this is used to send metrics to statsd
739 795 ; We recommend setting statsd_exported and scrape them using Prometheus
740 796 #statsd.enabled = false
741 797 #statsd.statsd_host = 0.0.0.0
742 798 #statsd.statsd_port = 8125
743 799 #statsd.statsd_prefix =
744 800 #statsd.statsd_ipv6 = false
745 801
746 802 ; configure logging automatically at server startup set to false
747 803 ; to use the below custom logging config.
748 804 ; RC_LOGGING_FORMATTER
749 805 ; RC_LOGGING_LEVEL
750 806 ; env variables can control the settings for logging in case of autoconfigure
751 807
752 808 #logging.autoconfigure = true
753 809
754 810 ; specify your own custom logging config file to configure logging
755 811 #logging.logging_conf_file = /path/to/custom_logging.ini
756 812
757 813 ; Dummy marker to add new entries after.
758 814 ; Add any custom entries below. Please don't remove this marker.
759 815 custom.conf = 1
760 816
761 817
762 818 ; #####################
763 819 ; LOGGING CONFIGURATION
764 820 ; #####################
765 821
766 822 [loggers]
767 823 keys = root, sqlalchemy, beaker, celery, rhodecode, ssh_wrapper
768 824
769 825 [handlers]
770 826 keys = console, console_sql
771 827
772 828 [formatters]
773 829 keys = generic, json, color_formatter, color_formatter_sql
774 830
775 831 ; #######
776 832 ; LOGGERS
777 833 ; #######
778 834 [logger_root]
779 835 level = NOTSET
780 836 handlers = console
781 837
782 838 [logger_sqlalchemy]
783 839 level = INFO
784 840 handlers = console_sql
785 841 qualname = sqlalchemy.engine
786 842 propagate = 0
787 843
788 844 [logger_beaker]
789 845 level = DEBUG
790 846 handlers =
791 847 qualname = beaker.container
792 848 propagate = 1
793 849
794 850 [logger_rhodecode]
795 851 level = DEBUG
796 852 handlers =
797 853 qualname = rhodecode
798 854 propagate = 1
799 855
800 856 [logger_ssh_wrapper]
801 857 level = DEBUG
802 858 handlers =
803 859 qualname = ssh_wrapper
804 860 propagate = 1
805 861
806 862 [logger_celery]
807 863 level = DEBUG
808 864 handlers =
809 865 qualname = celery
810 866
811 867
812 868 ; ########
813 869 ; HANDLERS
814 870 ; ########
815 871
816 872 [handler_console]
817 873 class = StreamHandler
818 874 args = (sys.stderr, )
819 875 level = DEBUG
820 876 ; To enable JSON formatted logs replace 'generic/color_formatter' with 'json'
821 877 ; This allows sending properly formatted logs to grafana loki or elasticsearch
822 878 formatter = color_formatter
823 879
824 880 [handler_console_sql]
825 881 ; "level = DEBUG" logs SQL queries and results.
826 882 ; "level = INFO" logs SQL queries.
827 883 ; "level = WARN" logs neither. (Recommended for production systems.)
828 884 class = StreamHandler
829 885 args = (sys.stderr, )
830 886 level = WARN
831 887 ; To enable JSON formatted logs replace 'generic/color_formatter_sql' with 'json'
832 888 ; This allows sending properly formatted logs to grafana loki or elasticsearch
833 889 formatter = color_formatter_sql
834 890
835 891 ; ##########
836 892 ; FORMATTERS
837 893 ; ##########
838 894
839 895 [formatter_generic]
840 896 class = rhodecode.lib.logging_formatter.ExceptionAwareFormatter
841 897 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
842 898 datefmt = %Y-%m-%d %H:%M:%S
843 899
844 900 [formatter_color_formatter]
845 901 class = rhodecode.lib.logging_formatter.ColorFormatter
846 902 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
847 903 datefmt = %Y-%m-%d %H:%M:%S
848 904
849 905 [formatter_color_formatter_sql]
850 906 class = rhodecode.lib.logging_formatter.ColorFormatterSql
851 907 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
852 908 datefmt = %Y-%m-%d %H:%M:%S
853 909
854 910 [formatter_json]
855 911 format = %(timestamp)s %(levelname)s %(name)s %(message)s %(req_id)s
856 912 class = rhodecode.lib._vendor.jsonlogger.JsonFormatter
@@ -1,520 +1,545 b''
1 1 """
2 2 Gunicorn config extension and hooks. This config file adds some extra settings and memory management.
3 3 Gunicorn configuration should be managed by .ini files entries of RhodeCode or VCSServer
4 4 """
5 5
6 6 import gc
7 7 import os
8 8 import sys
9 9 import math
10 10 import time
11 11 import threading
12 12 import traceback
13 13 import random
14 14 import socket
15 15 import dataclasses
16 import json
16 17 from gunicorn.glogging import Logger
17 18
18 19
19 20 def get_workers():
20 21 import multiprocessing
21 22 return multiprocessing.cpu_count() * 2 + 1
22 23
23 24
24 25 bind = "127.0.0.1:10020"
25 26
26 27
27 28 # Error logging output for gunicorn (-) is stdout
28 29 errorlog = '-'
29 30
30 31 # Access logging output for gunicorn (-) is stdout
31 32 accesslog = '-'
32 33
33 34
34 35 # SERVER MECHANICS
35 36 # None == system temp dir
36 37 # worker_tmp_dir is recommended to be set to some tmpfs
37 38 worker_tmp_dir = None
38 39 tmp_upload_dir = None
39 40
40 # use re-use port logic
41 #reuse_port = True
41 # use re-use port logic to let linux internals load-balance the requests better.
42 reuse_port = True
42 43
43 44 # Custom log format
44 45 #access_log_format = (
45 46 # '%(t)s %(p)s INFO [GNCRN] %(h)-15s rqt:%(L)s %(s)s %(b)-6s "%(m)s:%(U)s %(q)s" usr:%(u)s "%(f)s" "%(a)s"')
46 47
47 48 # loki format for easier parsing in grafana
48 access_log_format = (
49 loki_access_log_format = (
49 50 'time="%(t)s" pid=%(p)s level="INFO" type="[GNCRN]" ip="%(h)-15s" rqt="%(L)s" response_code="%(s)s" response_bytes="%(b)-6s" uri="%(m)s:%(U)s %(q)s" user=":%(u)s" user_agent="%(a)s"')
50 51
52 # JSON format
53 json_access_log_format = json.dumps({
54 'time': r'%(t)s',
55 'pid': r'%(p)s',
56 'level': 'INFO',
57 'ip': r'%(h)s',
58 'request_time': r'%(L)s',
59 'remote_address': r'%(h)s',
60 'user_name': r'%(u)s',
61 'status': r'%(s)s',
62 'method': r'%(m)s',
63 'url_path': r'%(U)s',
64 'query_string': r'%(q)s',
65 'protocol': r'%(H)s',
66 'response_length': r'%(B)s',
67 'referer': r'%(f)s',
68 'user_agent': r'%(a)s',
69
70 })
71
72 access_log_format = loki_access_log_format
73 if os.environ.get('RC_LOGGING_FORMATTER') == 'json':
74 access_log_format = json_access_log_format
75
51 76 # self adjust workers based on CPU count, to use maximum of CPU and not overquota the resources
52 77 # workers = get_workers()
53 78
54 79 # Gunicorn access log level
55 80 loglevel = 'info'
56 81
57 82 # Process name visible in a process list
58 83 proc_name = 'rhodecode_enterprise'
59 84
60 85 # Type of worker class, one of `sync`, `gevent` or `gthread`
61 86 # currently `sync` is the only option allowed for vcsserver and for rhodecode all of 3 are allowed
62 87 # gevent:
63 88 # In this case, the maximum number of concurrent requests is (N workers * X worker_connections)
64 89 # e.g. workers =3 worker_connections=10 = 3*10, 30 concurrent requests can be handled
65 90 # gthread:
66 91 # In this case, the maximum number of concurrent requests is (N workers * X threads)
67 92 # e.g. workers = 3 threads=3 = 3*3, 9 concurrent requests can be handled
68 93 worker_class = 'gthread'
69 94
70 95 # Sets the number of process workers. More workers means more concurrent connections
71 96 # RhodeCode can handle at the same time. Each additional worker also it increases
72 97 # memory usage as each has its own set of caches.
73 98 # The Recommended value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers, but no more
74 99 # than 8-10 unless for huge deployments .e.g 700-1000 users.
75 100 # `instance_id = *` must be set in the [app:main] section below (which is the default)
76 101 # when using more than 1 worker.
77 102 workers = 2
78 103
79 104 # Threads numbers for worker class gthread
80 105 threads = 1
81 106
82 107 # The maximum number of simultaneous clients. Valid only for gevent
83 108 # In this case, the maximum number of concurrent requests is (N workers * X worker_connections)
84 109 # e.g workers =3 worker_connections=10 = 3*10, 30 concurrent requests can be handled
85 110 worker_connections = 10
86 111
87 112 # Max number of requests that worker will handle before being gracefully restarted.
88 113 # Prevents memory leaks, jitter adds variability so not all workers are restarted at once.
89 114 max_requests = 2000
90 115 max_requests_jitter = int(max_requests * 0.2) # 20% of max_requests
91 116
92 117 # The maximum number of pending connections.
93 118 # Exceeding this number results in the client getting an error when attempting to connect.
94 119 backlog = 64
95 120
96 121 # The Amount of time a worker can spend with handling a request before it
97 122 # gets killed and restarted. By default, set to 21600 (6hrs)
98 123 # Examples: 1800 (30min), 3600 (1hr), 7200 (2hr), 43200 (12h)
99 124 timeout = 21600
100 125
101 126 # The maximum size of HTTP request line in bytes.
102 127 # 0 for unlimited
103 128 limit_request_line = 0
104 129
105 130 # Limit the number of HTTP headers fields in a request.
106 131 # By default this value is 100 and can't be larger than 32768.
107 132 limit_request_fields = 32768
108 133
109 134 # Limit the allowed size of an HTTP request header field.
110 135 # Value is a positive number or 0.
111 136 # Setting it to 0 will allow unlimited header field sizes.
112 137 limit_request_field_size = 0
113 138
114 139 # Timeout for graceful workers restart.
115 140 # After receiving a restart signal, workers have this much time to finish
116 141 # serving requests. Workers still alive after the timeout (starting from the
117 142 # receipt of the restart signal) are force killed.
118 143 # Examples: 1800 (30min), 3600 (1hr), 7200 (2hr), 43200 (12h)
119 144 graceful_timeout = 21600
120 145
121 146 # The number of seconds to wait for requests on a Keep-Alive connection.
122 147 # Generally set in the 1-5 seconds range.
123 148 keepalive = 2
124 149
125 150 # Maximum memory usage that each worker can use before it will receive a
126 151 # graceful restart signal 0 = memory monitoring is disabled
127 152 # Examples: 268435456 (256MB), 536870912 (512MB)
128 153 # 1073741824 (1GB), 2147483648 (2GB), 4294967296 (4GB)
129 154 # Dynamic formula 1024 * 1024 * 256 == 256MBs
130 155 memory_max_usage = 0
131 156
132 157 # How often in seconds to check for memory usage for each gunicorn worker
133 158 memory_usage_check_interval = 60
134 159
135 160 # Threshold value for which we don't recycle worker if GarbageCollection
136 161 # frees up enough resources. Before each restart, we try to run GC on worker
137 162 # in case we get enough free memory after that; restart will not happen.
138 163 memory_usage_recovery_threshold = 0.8
139 164
140 165
141 166 @dataclasses.dataclass
142 167 class MemoryCheckConfig:
143 168 max_usage: int
144 169 check_interval: int
145 170 recovery_threshold: float
146 171
147 172
148 173 def _get_process_rss(pid=None):
149 174 try:
150 175 import psutil
151 176 if pid:
152 177 proc = psutil.Process(pid)
153 178 else:
154 179 proc = psutil.Process()
155 180 return proc.memory_info().rss
156 181 except Exception:
157 182 return None
158 183
159 184
160 185 def _get_config(ini_path):
161 186 import configparser
162 187
163 188 try:
164 189 config = configparser.RawConfigParser()
165 190 config.read(ini_path)
166 191 return config
167 192 except Exception:
168 193 return None
169 194
170 195
171 196 def get_memory_usage_params(config=None):
172 197 # memory spec defaults
173 198 _memory_max_usage = memory_max_usage
174 199 _memory_usage_check_interval = memory_usage_check_interval
175 200 _memory_usage_recovery_threshold = memory_usage_recovery_threshold
176 201
177 202 if config:
178 203 ini_path = os.path.abspath(config)
179 204 conf = _get_config(ini_path)
180 205
181 206 section = 'server:main'
182 207 if conf and conf.has_section(section):
183 208
184 209 if conf.has_option(section, 'memory_max_usage'):
185 210 _memory_max_usage = conf.getint(section, 'memory_max_usage')
186 211
187 212 if conf.has_option(section, 'memory_usage_check_interval'):
188 213 _memory_usage_check_interval = conf.getint(section, 'memory_usage_check_interval')
189 214
190 215 if conf.has_option(section, 'memory_usage_recovery_threshold'):
191 216 _memory_usage_recovery_threshold = conf.getfloat(section, 'memory_usage_recovery_threshold')
192 217
193 218 _memory_max_usage = int(os.environ.get('RC_GUNICORN_MEMORY_MAX_USAGE', '')
194 219 or _memory_max_usage)
195 220 _memory_usage_check_interval = int(os.environ.get('RC_GUNICORN_MEMORY_USAGE_CHECK_INTERVAL', '')
196 221 or _memory_usage_check_interval)
197 222 _memory_usage_recovery_threshold = float(os.environ.get('RC_GUNICORN_MEMORY_USAGE_RECOVERY_THRESHOLD', '')
198 223 or _memory_usage_recovery_threshold)
199 224
200 225 return MemoryCheckConfig(_memory_max_usage, _memory_usage_check_interval, _memory_usage_recovery_threshold)
201 226
202 227
203 228 def _time_with_offset(check_interval):
204 229 return time.time() - random.randint(0, check_interval/2.0)
205 230
206 231
207 232 def pre_fork(server, worker):
208 233 pass
209 234
210 235
211 236 def post_fork(server, worker):
212 237
213 238 memory_conf = get_memory_usage_params()
214 239 _memory_max_usage = memory_conf.max_usage
215 240 _memory_usage_check_interval = memory_conf.check_interval
216 241 _memory_usage_recovery_threshold = memory_conf.recovery_threshold
217 242
218 243 worker._memory_max_usage = int(os.environ.get('RC_GUNICORN_MEMORY_MAX_USAGE', '')
219 244 or _memory_max_usage)
220 245 worker._memory_usage_check_interval = int(os.environ.get('RC_GUNICORN_MEMORY_USAGE_CHECK_INTERVAL', '')
221 246 or _memory_usage_check_interval)
222 247 worker._memory_usage_recovery_threshold = float(os.environ.get('RC_GUNICORN_MEMORY_USAGE_RECOVERY_THRESHOLD', '')
223 248 or _memory_usage_recovery_threshold)
224 249
225 250 # register memory last check time, with some random offset so we don't recycle all
226 251 # at once
227 252 worker._last_memory_check_time = _time_with_offset(_memory_usage_check_interval)
228 253
229 254 if _memory_max_usage:
230 255 server.log.info("pid=[%-10s] WORKER spawned with max memory set at %s", worker.pid,
231 256 _format_data_size(_memory_max_usage))
232 257 else:
233 258 server.log.info("pid=[%-10s] WORKER spawned", worker.pid)
234 259
235 260
236 261 def pre_exec(server):
237 262 server.log.info("Forked child, re-executing.")
238 263
239 264
240 265 def on_starting(server):
241 266 server_lbl = '{} {}'.format(server.proc_name, server.address)
242 267 server.log.info("Server %s is starting.", server_lbl)
243 268 server.log.info('Config:')
244 269 server.log.info(f"\n{server.cfg}")
245 270 server.log.info(get_memory_usage_params())
246 271
247 272
248 273 def when_ready(server):
249 274 server.log.info("Server %s is ready. Spawning workers", server)
250 275
251 276
252 277 def on_reload(server):
253 278 pass
254 279
255 280
256 281 def _format_data_size(size, unit="B", precision=1, binary=True):
257 282 """Format a number using SI units (kilo, mega, etc.).
258 283
259 284 ``size``: The number as a float or int.
260 285
261 286 ``unit``: The unit name in plural form. Examples: "bytes", "B".
262 287
263 288 ``precision``: How many digits to the right of the decimal point. Default
264 289 is 1. 0 suppresses the decimal point.
265 290
266 291 ``binary``: If false, use base-10 decimal prefixes (kilo = K = 1000).
267 292 If true, use base-2 binary prefixes (kibi = Ki = 1024).
268 293
269 294 ``full_name``: If false (default), use the prefix abbreviation ("k" or
270 295 "Ki"). If true, use the full prefix ("kilo" or "kibi"). If false,
271 296 use abbreviation ("k" or "Ki").
272 297
273 298 """
274 299
275 300 if not binary:
276 301 base = 1000
277 302 multiples = ('', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
278 303 else:
279 304 base = 1024
280 305 multiples = ('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi')
281 306
282 307 sign = ""
283 308 if size > 0:
284 309 m = int(math.log(size, base))
285 310 elif size < 0:
286 311 sign = "-"
287 312 size = -size
288 313 m = int(math.log(size, base))
289 314 else:
290 315 m = 0
291 316 if m > 8:
292 317 m = 8
293 318
294 319 if m == 0:
295 320 precision = '%.0f'
296 321 else:
297 322 precision = '%%.%df' % precision
298 323
299 324 size = precision % (size / math.pow(base, m))
300 325
301 326 return '%s%s %s%s' % (sign, size.strip(), multiples[m], unit)
302 327
303 328
304 329 def _check_memory_usage(worker):
305 330 _memory_max_usage = worker._memory_max_usage
306 331 if not _memory_max_usage:
307 332 return
308 333
309 334 _memory_usage_check_interval = worker._memory_usage_check_interval
310 335 _memory_usage_recovery_threshold = memory_max_usage * worker._memory_usage_recovery_threshold
311 336
312 337 elapsed = time.time() - worker._last_memory_check_time
313 338 if elapsed > _memory_usage_check_interval:
314 339 mem_usage = _get_process_rss()
315 340 if mem_usage and mem_usage > _memory_max_usage:
316 341 worker.log.info(
317 342 "memory usage %s > %s, forcing gc",
318 343 _format_data_size(mem_usage), _format_data_size(_memory_max_usage))
319 344 # Try to clean it up by forcing a full collection.
320 345 gc.collect()
321 346 mem_usage = _get_process_rss()
322 347 if mem_usage > _memory_usage_recovery_threshold:
323 348 # Didn't clean up enough, we'll have to terminate.
324 349 worker.log.warning(
325 350 "memory usage %s > %s after gc, quitting",
326 351 _format_data_size(mem_usage), _format_data_size(_memory_max_usage))
327 352 # This will cause worker to auto-restart itself
328 353 worker.alive = False
329 354 worker._last_memory_check_time = time.time()
330 355
331 356
332 357 def worker_int(worker):
333 358 worker.log.info("pid=[%-10s] worker received INT or QUIT signal", worker.pid)
334 359
335 360 # get traceback info, when a worker crashes
336 361 def get_thread_id(t_id):
337 362 id2name = dict([(th.ident, th.name) for th in threading.enumerate()])
338 363 return id2name.get(t_id, "unknown_thread_id")
339 364
340 365 code = []
341 366 for thread_id, stack in sys._current_frames().items(): # noqa
342 367 code.append(
343 368 "\n# Thread: %s(%d)" % (get_thread_id(thread_id), thread_id))
344 369 for fname, lineno, name, line in traceback.extract_stack(stack):
345 370 code.append('File: "%s", line %d, in %s' % (fname, lineno, name))
346 371 if line:
347 372 code.append(" %s" % (line.strip()))
348 373 worker.log.debug("\n".join(code))
349 374
350 375
351 376 def worker_abort(worker):
352 377 worker.log.info("pid=[%-10s] worker received SIGABRT signal", worker.pid)
353 378
354 379
355 380 def worker_exit(server, worker):
356 381 worker.log.info("pid=[%-10s] worker exit", worker.pid)
357 382
358 383
359 384 def child_exit(server, worker):
360 385 worker.log.info("pid=[%-10s] worker child exit", worker.pid)
361 386
362 387
363 388 def pre_request(worker, req):
364 389 worker.start_time = time.time()
365 390 worker.log.debug(
366 391 "GNCRN PRE WORKER [cnt:%s]: %s %s", worker.nr, req.method, req.path)
367 392
368 393
369 394 def post_request(worker, req, environ, resp):
370 395 total_time = time.time() - worker.start_time
371 396 # Gunicorn sometimes has problems with reading the status_code
372 397 status_code = getattr(resp, 'status_code', '')
373 398 worker.log.debug(
374 399 "GNCRN POST WORKER [cnt:%s]: %s %s resp: %s, Load Time: %.4fs",
375 400 worker.nr, req.method, req.path, status_code, total_time)
376 401 _check_memory_usage(worker)
377 402
378 403
379 404 def _filter_proxy(ip):
380 405 """
381 406 Passed in IP addresses in HEADERS can be in a special format of multiple
382 407 ips. Those comma separated IPs are passed from various proxies in the
383 408 chain of request processing. The left-most being the original client.
384 409 We only care about the first IP which came from the org. client.
385 410
386 411 :param ip: ip string from headers
387 412 """
388 413 if ',' in ip:
389 414 _ips = ip.split(',')
390 415 _first_ip = _ips[0].strip()
391 416 return _first_ip
392 417 return ip
393 418
394 419
395 420 def _filter_port(ip):
396 421 """
397 422 Removes a port from ip, there are 4 main cases to handle here.
398 423 - ipv4 eg. 127.0.0.1
399 424 - ipv6 eg. ::1
400 425 - ipv4+port eg. 127.0.0.1:8080
401 426 - ipv6+port eg. [::1]:8080
402 427
403 428 :param ip:
404 429 """
405 430 def is_ipv6(ip_addr):
406 431 if hasattr(socket, 'inet_pton'):
407 432 try:
408 433 socket.inet_pton(socket.AF_INET6, ip_addr)
409 434 except socket.error:
410 435 return False
411 436 else:
412 437 return False
413 438 return True
414 439
415 440 if ':' not in ip: # must be ipv4 pure ip
416 441 return ip
417 442
418 443 if '[' in ip and ']' in ip: # ipv6 with port
419 444 return ip.split(']')[0][1:].lower()
420 445
421 446 # must be ipv6 or ipv4 with port
422 447 if is_ipv6(ip):
423 448 return ip
424 449 else:
425 450 ip, _port = ip.split(':')[:2] # means ipv4+port
426 451 return ip
427 452
428 453
429 454 def get_ip_addr(environ):
430 455 proxy_key = 'HTTP_X_REAL_IP'
431 456 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
432 457 def_key = 'REMOTE_ADDR'
433 458
434 459 def _filters(x):
435 460 return _filter_port(_filter_proxy(x))
436 461
437 462 ip = environ.get(proxy_key)
438 463 if ip:
439 464 return _filters(ip)
440 465
441 466 ip = environ.get(proxy_key2)
442 467 if ip:
443 468 return _filters(ip)
444 469
445 470 ip = environ.get(def_key, '0.0.0.0')
446 471 return _filters(ip)
447 472
448 473
449 474 class RhodeCodeLogger(Logger):
450 475 """
451 476 Custom Logger that allows some customization that gunicorn doesn't allow
452 477 """
453 478
454 479 datefmt = r"%Y-%m-%d %H:%M:%S"
455 480
456 481 def __init__(self, cfg):
457 482 Logger.__init__(self, cfg)
458 483
459 484 def now(self):
460 485 """ return date in RhodeCode Log format """
461 486 now = time.time()
462 487 msecs = int((now - int(now)) * 1000)
463 488 return time.strftime(self.datefmt, time.localtime(now)) + '.{0:03d}'.format(msecs)
464 489
465 490 def atoms(self, resp, req, environ, request_time):
466 491 """ Gets atoms for log formatting.
467 492 """
468 493 status = resp.status
469 494 if isinstance(status, str):
470 495 status = status.split(None, 1)[0]
471 496 atoms = {
472 497 'h': get_ip_addr(environ),
473 498 'l': '-',
474 499 'u': self._get_user(environ) or '-',
475 500 't': self.now(),
476 501 'r': "%s %s %s" % (environ['REQUEST_METHOD'],
477 502 environ['RAW_URI'],
478 503 environ["SERVER_PROTOCOL"]),
479 504 's': status,
480 505 'm': environ.get('REQUEST_METHOD'),
481 506 'U': environ.get('PATH_INFO'),
482 507 'q': environ.get('QUERY_STRING'),
483 508 'H': environ.get('SERVER_PROTOCOL'),
484 509 'b': getattr(resp, 'sent', None) is not None and str(resp.sent) or '-',
485 510 'B': getattr(resp, 'sent', None),
486 511 'f': environ.get('HTTP_REFERER', '-'),
487 512 'a': environ.get('HTTP_USER_AGENT', '-'),
488 513 'T': request_time.seconds,
489 514 'D': (request_time.seconds * 1000000) + request_time.microseconds,
490 515 'M': (request_time.seconds * 1000) + int(request_time.microseconds/1000),
491 516 'L': "%d.%06d" % (request_time.seconds, request_time.microseconds),
492 517 'p': "<%s>" % os.getpid()
493 518 }
494 519
495 520 # add request headers
496 521 if hasattr(req, 'headers'):
497 522 req_headers = req.headers
498 523 else:
499 524 req_headers = req
500 525
501 526 if hasattr(req_headers, "items"):
502 527 req_headers = req_headers.items()
503 528
504 529 atoms.update({"{%s}i" % k.lower(): v for k, v in req_headers})
505 530
506 531 resp_headers = resp.headers
507 532 if hasattr(resp_headers, "items"):
508 533 resp_headers = resp_headers.items()
509 534
510 535 # add response headers
511 536 atoms.update({"{%s}o" % k.lower(): v for k, v in resp_headers})
512 537
513 538 # add environ variables
514 539 environ_variables = environ.items()
515 540 atoms.update({"{%s}e" % k.lower(): v for k, v in environ_variables})
516 541
517 542 return atoms
518 543
519 544
520 545 logger_class = RhodeCodeLogger
@@ -1,824 +1,880 b''
1 1
2 2 ; #########################################
3 3 ; RHODECODE COMMUNITY EDITION CONFIGURATION
4 4 ; #########################################
5 5
6 6 [DEFAULT]
7 7 ; Debug flag sets all loggers to debug, and enables request tracking
8 8 debug = false
9 9
10 10 ; ########################################################################
11 11 ; EMAIL CONFIGURATION
12 12 ; These settings will be used by the RhodeCode mailing system
13 13 ; ########################################################################
14 14
15 15 ; prefix all emails subjects with given prefix, helps filtering out emails
16 16 #email_prefix = [RhodeCode]
17 17
18 18 ; email FROM address all mails will be sent
19 19 #app_email_from = rhodecode-noreply@localhost
20 20
21 21 #smtp_server = mail.server.com
22 22 #smtp_username =
23 23 #smtp_password =
24 24 #smtp_port =
25 25 #smtp_use_tls = false
26 26 #smtp_use_ssl = true
27 27
28 28 [server:main]
29 29 ; COMMON HOST/IP CONFIG, This applies mostly to develop setup,
30 30 ; Host port for gunicorn are controlled by gunicorn_conf.py
31 31 host = 127.0.0.1
32 32 port = 10020
33 33
34 34
35 35 ; ###########################
36 36 ; GUNICORN APPLICATION SERVER
37 37 ; ###########################
38 38
39 39 ; run with gunicorn --config gunicorn_conf.py --paste rhodecode.ini
40 40
41 41 ; Module to use, this setting shouldn't be changed
42 42 use = egg:gunicorn#main
43 43
44 44 ; Prefix middleware for RhodeCode.
45 45 ; recommended when using proxy setup.
46 46 ; allows to set RhodeCode under a prefix in server.
47 47 ; eg https://server.com/custom_prefix. Enable `filter-with =` option below as well.
48 48 ; And set your prefix like: `prefix = /custom_prefix`
49 49 ; be sure to also set beaker.session.cookie_path = /custom_prefix if you need
50 50 ; to make your cookies only work on prefix url
51 51 [filter:proxy-prefix]
52 52 use = egg:PasteDeploy#prefix
53 53 prefix = /
54 54
55 55 [app:main]
56 56 ; The %(here)s variable will be replaced with the absolute path of parent directory
57 57 ; of this file
58 58 ; Each option in the app:main can be override by an environmental variable
59 59 ;
60 60 ;To override an option:
61 61 ;
62 62 ;RC_<KeyName>
63 63 ;Everything should be uppercase, . and - should be replaced by _.
64 64 ;For example, if you have these configuration settings:
65 65 ;rc_cache.repo_object.backend = foo
66 66 ;can be overridden by
67 67 ;export RC_CACHE_REPO_OBJECT_BACKEND=foo
68 68
69 69 use = egg:rhodecode-enterprise-ce
70 70
71 71 ; enable proxy prefix middleware, defined above
72 72 #filter-with = proxy-prefix
73 73
74 74 ; encryption key used to encrypt social plugin tokens,
75 75 ; remote_urls with credentials etc, if not set it defaults to
76 76 ; `beaker.session.secret`
77 77 #rhodecode.encrypted_values.secret =
78 78
79 79 ; decryption strict mode (enabled by default). It controls if decryption raises
80 80 ; `SignatureVerificationError` in case of wrong key, or damaged encryption data.
81 81 #rhodecode.encrypted_values.strict = false
82 82
83 83 ; Pick algorithm for encryption. Either fernet (more secure) or aes (default)
84 84 ; fernet is safer, and we strongly recommend switching to it.
85 85 ; Due to backward compatibility aes is used as default.
86 86 #rhodecode.encrypted_values.algorithm = fernet
87 87
88 88 ; Return gzipped responses from RhodeCode (static files/application)
89 89 gzip_responses = false
90 90
91 91 ; Auto-generate javascript routes file on startup
92 92 generate_js_files = false
93 93
94 94 ; System global default language.
95 95 ; All available languages: en (default), be, de, es, fr, it, ja, pl, pt, ru, zh
96 96 lang = en
97 97
98 98 ; Perform a full repository scan and import on each server start.
99 99 ; Settings this to true could lead to very long startup time.
100 100 startup.import_repos = false
101 101
102 102 ; URL at which the application is running. This is used for Bootstrapping
103 103 ; requests in context when no web request is available. Used in ishell, or
104 104 ; SSH calls. Set this for events to receive proper url for SSH calls.
105 105 app.base_url = http://rhodecode.local
106 106
107 107 ; Host at which the Service API is running.
108 108 app.service_api.host = http://rhodecode.local:10020
109 109
110 110 ; Secret for Service API authentication.
111 111 app.service_api.token =
112 112
113 113 ; Unique application ID. Should be a random unique string for security.
114 114 app_instance_uuid = rc-production
115 115
116 116 ; Cut off limit for large diffs (size in bytes). If overall diff size on
117 117 ; commit, or pull request exceeds this limit this diff will be displayed
118 118 ; partially. E.g 512000 == 512Kb
119 119 cut_off_limit_diff = 512000
120 120
121 121 ; Cut off limit for large files inside diffs (size in bytes). Each individual
122 122 ; file inside diff which exceeds this limit will be displayed partially.
123 123 ; E.g 128000 == 128Kb
124 124 cut_off_limit_file = 128000
125 125
126 126 ; Use cached version of vcs repositories everywhere. Recommended to be `true`
127 127 vcs_full_cache = true
128 128
129 129 ; Force https in RhodeCode, fixes https redirects, assumes it's always https.
130 130 ; Normally this is controlled by proper flags sent from http server such as Nginx or Apache
131 131 force_https = false
132 132
133 133 ; use Strict-Transport-Security headers
134 134 use_htsts = false
135 135
136 136 ; Set to true if your repos are exposed using the dumb protocol
137 137 git_update_server_info = false
138 138
139 139 ; RSS/ATOM feed options
140 140 rss_cut_off_limit = 256000
141 141 rss_items_per_page = 10
142 142 rss_include_diff = false
143 143
144 144 ; gist URL alias, used to create nicer urls for gist. This should be an
145 145 ; url that does rewrites to _admin/gists/{gistid}.
146 146 ; example: http://gist.rhodecode.org/{gistid}. Empty means use the internal
147 147 ; RhodeCode url, ie. http[s]://rhodecode.server/_admin/gists/{gistid}
148 148 gist_alias_url =
149 149
150 150 ; List of views (using glob pattern syntax) that AUTH TOKENS could be
151 151 ; used for access.
152 152 ; Adding ?auth_token=TOKEN_HASH to the url authenticates this request as if it
153 153 ; came from the the logged in user who own this authentication token.
154 154 ; Additionally @TOKEN syntax can be used to bound the view to specific
155 155 ; authentication token. Such view would be only accessible when used together
156 156 ; with this authentication token
157 157 ; list of all views can be found under `/_admin/permissions/auth_token_access`
158 158 ; The list should be "," separated and on a single line.
159 159 ; Most common views to enable:
160 160
161 161 # RepoCommitsView:repo_commit_download
162 162 # RepoCommitsView:repo_commit_patch
163 163 # RepoCommitsView:repo_commit_raw
164 164 # RepoCommitsView:repo_commit_raw@TOKEN
165 165 # RepoFilesView:repo_files_diff
166 166 # RepoFilesView:repo_archivefile
167 167 # RepoFilesView:repo_file_raw
168 168 # GistView:*
169 169 api_access_controllers_whitelist =
170 170
171 171 ; Default encoding used to convert from and to unicode
172 172 ; can be also a comma separated list of encoding in case of mixed encodings
173 173 default_encoding = UTF-8
174 174
175 175 ; instance-id prefix
176 176 ; a prefix key for this instance used for cache invalidation when running
177 177 ; multiple instances of RhodeCode, make sure it's globally unique for
178 178 ; all running RhodeCode instances. Leave empty if you don't use it
179 179 instance_id =
180 180
181 181 ; Fallback authentication plugin. Set this to a plugin ID to force the usage
182 182 ; of an authentication plugin also if it is disabled by it's settings.
183 183 ; This could be useful if you are unable to log in to the system due to broken
184 184 ; authentication settings. Then you can enable e.g. the internal RhodeCode auth
185 185 ; module to log in again and fix the settings.
186 186 ; Available builtin plugin IDs (hash is part of the ID):
187 187 ; egg:rhodecode-enterprise-ce#rhodecode
188 188 ; egg:rhodecode-enterprise-ce#pam
189 189 ; egg:rhodecode-enterprise-ce#ldap
190 190 ; egg:rhodecode-enterprise-ce#jasig_cas
191 191 ; egg:rhodecode-enterprise-ce#headers
192 192 ; egg:rhodecode-enterprise-ce#crowd
193 193
194 194 #rhodecode.auth_plugin_fallback = egg:rhodecode-enterprise-ce#rhodecode
195 195
196 196 ; Flag to control loading of legacy plugins in py:/path format
197 197 auth_plugin.import_legacy_plugins = true
198 198
199 199 ; alternative return HTTP header for failed authentication. Default HTTP
200 200 ; response is 401 HTTPUnauthorized. Currently HG clients have troubles with
201 201 ; handling that causing a series of failed authentication calls.
202 202 ; Set this variable to 403 to return HTTPForbidden, or any other HTTP code
203 203 ; This will be served instead of default 401 on bad authentication
204 204 auth_ret_code =
205 205
206 206 ; use special detection method when serving auth_ret_code, instead of serving
207 207 ; ret_code directly, use 401 initially (Which triggers credentials prompt)
208 208 ; and then serve auth_ret_code to clients
209 209 auth_ret_code_detection = false
210 210
211 211 ; locking return code. When repository is locked return this HTTP code. 2XX
212 212 ; codes don't break the transactions while 4XX codes do
213 213 lock_ret_code = 423
214 214
215 215 ; Filesystem location were repositories should be stored
216 216 repo_store.path = /var/opt/rhodecode_repo_store
217 217
218 218 ; allows to setup custom hooks in settings page
219 219 allow_custom_hooks_settings = true
220 220
221 221 ; Generated license token required for EE edition license.
222 222 ; New generated token value can be found in Admin > settings > license page.
223 223 license_token =
224 224
225 225 ; This flag hides sensitive information on the license page such as token, and license data
226 226 license.hide_license_info = false
227 227
228 ; Import EE license from this license path
229 #license.import_path = %(here)s/rhodecode_enterprise.license
230
231 ; import license 'if-missing' or 'force' (always override)
232 ; if-missing means apply license if it doesn't exist. 'force' option always overrides it
233 license.import_path_mode = if-missing
234
228 235 ; supervisor connection uri, for managing supervisor and logs.
229 236 supervisor.uri =
230 237
231 238 ; supervisord group name/id we only want this RC instance to handle
232 239 supervisor.group_id = prod
233 240
234 241 ; Display extended labs settings
235 242 labs_settings_active = true
236 243
237 244 ; Custom exception store path, defaults to TMPDIR
238 245 ; This is used to store exception from RhodeCode in shared directory
239 246 #exception_tracker.store_path =
240 247
241 248 ; Send email with exception details when it happens
242 249 #exception_tracker.send_email = false
243 250
244 251 ; Comma separated list of recipients for exception emails,
245 252 ; e.g admin@rhodecode.com,devops@rhodecode.com
246 253 ; Can be left empty, then emails will be sent to ALL super-admins
247 254 #exception_tracker.send_email_recipients =
248 255
249 256 ; optional prefix to Add to email Subject
250 257 #exception_tracker.email_prefix = [RHODECODE ERROR]
251 258
252 ; File store configuration. This is used to store and serve uploaded files
253 file_store.enabled = true
259 ; NOTE: this setting IS DEPRECATED:
260 ; file_store backend is always enabled
261 #file_store.enabled = true
254 262
263 ; NOTE: this setting IS DEPRECATED:
264 ; file_store.backend = X -> use `file_store.backend.type = filesystem_v2` instead
255 265 ; Storage backend, available options are: local
256 file_store.backend = local
266 #file_store.backend = local
257 267
268 ; NOTE: this setting IS DEPRECATED:
269 ; file_store.storage_path = X -> use `file_store.filesystem_v2.storage_path = X` instead
258 270 ; path to store the uploaded binaries and artifacts
259 file_store.storage_path = /var/opt/rhodecode_data/file_store
271 #file_store.storage_path = /var/opt/rhodecode_data/file_store
272
273 ; Artifacts file-store, is used to store comment attachments and artifacts uploads.
274 ; file_store backend type: filesystem_v1, filesystem_v2 or objectstore (s3-based) are available as options
275 ; filesystem_v1 is backwards compat with pre 5.1 storage changes
276 ; new installations should choose filesystem_v2 or objectstore (s3-based), pick filesystem when migrating from
277 ; previous installations to keep the artifacts without a need of migration
278 #file_store.backend.type = filesystem_v2
279
280 ; filesystem options...
281 #file_store.filesystem_v1.storage_path = /var/opt/rhodecode_data/artifacts_file_store
282
283 ; filesystem_v2 options...
284 #file_store.filesystem_v2.storage_path = /var/opt/rhodecode_data/artifacts_file_store
285 #file_store.filesystem_v2.shards = 8
260 286
287 ; objectstore options...
288 ; url for s3 compatible storage that allows to upload artifacts
289 ; e.g http://minio:9000
290 #file_store.backend.type = objectstore
291 #file_store.objectstore.url = http://s3-minio:9000
292
293 ; a top-level bucket to put all other shards in
294 ; objects will be stored in rhodecode-file-store/shard-N based on the bucket_shards number
295 #file_store.objectstore.bucket = rhodecode-file-store
296
297 ; number of sharded buckets to create to distribute archives across
298 ; default is 8 shards
299 #file_store.objectstore.bucket_shards = 8
300
301 ; key for s3 auth
302 #file_store.objectstore.key = s3admin
303
304 ; secret for s3 auth
305 #file_store.objectstore.secret = s3secret4
306
307 ;region for s3 storage
308 #file_store.objectstore.region = eu-central-1
261 309
262 310 ; Redis url to acquire/check generation of archives locks
263 311 archive_cache.locking.url = redis://redis:6379/1
264 312
265 313 ; Storage backend, only 'filesystem' and 'objectstore' are available now
266 314 archive_cache.backend.type = filesystem
267 315
268 316 ; url for s3 compatible storage that allows to upload artifacts
269 317 ; e.g http://minio:9000
270 318 archive_cache.objectstore.url = http://s3-minio:9000
271 319
272 320 ; key for s3 auth
273 321 archive_cache.objectstore.key = key
274 322
275 323 ; secret for s3 auth
276 324 archive_cache.objectstore.secret = secret
277 325
278 326 ;region for s3 storage
279 327 archive_cache.objectstore.region = eu-central-1
280 328
281 329 ; number of sharded buckets to create to distribute archives across
282 330 ; default is 8 shards
283 331 archive_cache.objectstore.bucket_shards = 8
284 332
285 333 ; a top-level bucket to put all other shards in
286 334 ; objects will be stored in rhodecode-archive-cache/shard-N based on the bucket_shards number
287 335 archive_cache.objectstore.bucket = rhodecode-archive-cache
288 336
289 337 ; if true, this cache will try to retry with retry_attempts=N times waiting retry_backoff time
290 338 archive_cache.objectstore.retry = false
291 339
292 340 ; number of seconds to wait for next try using retry
293 341 archive_cache.objectstore.retry_backoff = 1
294 342
295 343 ; how many tries do do a retry fetch from this backend
296 344 archive_cache.objectstore.retry_attempts = 10
297 345
298 346 ; Default is $cache_dir/archive_cache if not set
299 347 ; Generated repo archives will be cached at this location
300 348 ; and served from the cache during subsequent requests for the same archive of
301 349 ; the repository. This path is important to be shared across filesystems and with
302 350 ; RhodeCode and vcsserver
303 351 archive_cache.filesystem.store_dir = /var/opt/rhodecode_data/archive_cache
304 352
305 353 ; The limit in GB sets how much data we cache before recycling last used, defaults to 10 gb
306 354 archive_cache.filesystem.cache_size_gb = 40
307 355
308 356 ; Eviction policy used to clear out after cache_size_gb limit is reached
309 357 archive_cache.filesystem.eviction_policy = least-recently-stored
310 358
311 359 ; By default cache uses sharding technique, this specifies how many shards are there
312 360 ; default is 8 shards
313 361 archive_cache.filesystem.cache_shards = 8
314 362
315 363 ; if true, this cache will try to retry with retry_attempts=N times waiting retry_backoff time
316 364 archive_cache.filesystem.retry = false
317 365
318 366 ; number of seconds to wait for next try using retry
319 367 archive_cache.filesystem.retry_backoff = 1
320 368
321 369 ; how many tries do do a retry fetch from this backend
322 370 archive_cache.filesystem.retry_attempts = 10
323 371
324 372
325 373 ; #############
326 374 ; CELERY CONFIG
327 375 ; #############
328 376
329 377 ; manually run celery: /path/to/celery worker --task-events --beat --app rhodecode.lib.celerylib.loader --scheduler rhodecode.lib.celerylib.scheduler.RcScheduler --loglevel DEBUG --ini /path/to/rhodecode.ini
330 378
331 379 use_celery = true
332 380
333 381 ; path to store schedule database
334 382 #celerybeat-schedule.path =
335 383
336 384 ; connection url to the message broker (default redis)
337 385 celery.broker_url = redis://redis:6379/8
338 386
339 387 ; results backend to get results for (default redis)
340 388 celery.result_backend = redis://redis:6379/8
341 389
342 390 ; rabbitmq example
343 391 #celery.broker_url = amqp://rabbitmq:qweqwe@localhost:5672/rabbitmqhost
344 392
345 393 ; maximum tasks to execute before worker restart
346 394 celery.max_tasks_per_child = 20
347 395
348 396 ; tasks will never be sent to the queue, but executed locally instead.
349 397 celery.task_always_eager = false
350 398
351 399 ; #############
352 400 ; DOGPILE CACHE
353 401 ; #############
354 402
355 403 ; Default cache dir for caches. Putting this into a ramdisk can boost performance.
356 404 ; eg. /tmpfs/data_ramdisk, however this directory might require large amount of space
357 405 cache_dir = /var/opt/rhodecode_data
358 406
359 407 ; *********************************************
360 408 ; `sql_cache_short` cache for heavy SQL queries
361 409 ; Only supported backend is `memory_lru`
362 410 ; *********************************************
363 411 rc_cache.sql_cache_short.backend = dogpile.cache.rc.memory_lru
364 412 rc_cache.sql_cache_short.expiration_time = 30
365 413
366 414
367 415 ; *****************************************************
368 416 ; `cache_repo_longterm` cache for repo object instances
369 417 ; Only supported backend is `memory_lru`
370 418 ; *****************************************************
371 419 rc_cache.cache_repo_longterm.backend = dogpile.cache.rc.memory_lru
372 420 ; by default we use 30 Days, cache is still invalidated on push
373 421 rc_cache.cache_repo_longterm.expiration_time = 2592000
374 422 ; max items in LRU cache, set to smaller number to save memory, and expire last used caches
375 423 rc_cache.cache_repo_longterm.max_size = 10000
376 424
377 425
378 426 ; *********************************************
379 427 ; `cache_general` cache for general purpose use
380 428 ; for simplicity use rc.file_namespace backend,
381 429 ; for performance and scale use rc.redis
382 430 ; *********************************************
383 431 rc_cache.cache_general.backend = dogpile.cache.rc.file_namespace
384 432 rc_cache.cache_general.expiration_time = 43200
385 433 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set
386 434 #rc_cache.cache_general.arguments.filename = /tmp/cache_general_db
387 435
388 436 ; alternative `cache_general` redis backend with distributed lock
389 437 #rc_cache.cache_general.backend = dogpile.cache.rc.redis
390 438 #rc_cache.cache_general.expiration_time = 300
391 439
392 440 ; redis_expiration_time needs to be greater then expiration_time
393 441 #rc_cache.cache_general.arguments.redis_expiration_time = 7200
394 442
395 443 #rc_cache.cache_general.arguments.host = localhost
396 444 #rc_cache.cache_general.arguments.port = 6379
397 445 #rc_cache.cache_general.arguments.db = 0
398 446 #rc_cache.cache_general.arguments.socket_timeout = 30
399 447 ; more Redis options: https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends
400 448 #rc_cache.cache_general.arguments.distributed_lock = true
401 449
402 450 ; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen
403 451 #rc_cache.cache_general.arguments.lock_auto_renewal = true
404 452
405 453 ; *************************************************
406 454 ; `cache_perms` cache for permission tree, auth TTL
407 455 ; for simplicity use rc.file_namespace backend,
408 456 ; for performance and scale use rc.redis
409 457 ; *************************************************
410 458 rc_cache.cache_perms.backend = dogpile.cache.rc.file_namespace
411 459 rc_cache.cache_perms.expiration_time = 3600
412 460 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set
413 461 #rc_cache.cache_perms.arguments.filename = /tmp/cache_perms_db
414 462
415 463 ; alternative `cache_perms` redis backend with distributed lock
416 464 #rc_cache.cache_perms.backend = dogpile.cache.rc.redis
417 465 #rc_cache.cache_perms.expiration_time = 300
418 466
419 467 ; redis_expiration_time needs to be greater then expiration_time
420 468 #rc_cache.cache_perms.arguments.redis_expiration_time = 7200
421 469
422 470 #rc_cache.cache_perms.arguments.host = localhost
423 471 #rc_cache.cache_perms.arguments.port = 6379
424 472 #rc_cache.cache_perms.arguments.db = 0
425 473 #rc_cache.cache_perms.arguments.socket_timeout = 30
426 474 ; more Redis options: https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends
427 475 #rc_cache.cache_perms.arguments.distributed_lock = true
428 476
429 477 ; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen
430 478 #rc_cache.cache_perms.arguments.lock_auto_renewal = true
431 479
432 480 ; ***************************************************
433 481 ; `cache_repo` cache for file tree, Readme, RSS FEEDS
434 482 ; for simplicity use rc.file_namespace backend,
435 483 ; for performance and scale use rc.redis
436 484 ; ***************************************************
437 485 rc_cache.cache_repo.backend = dogpile.cache.rc.file_namespace
438 486 rc_cache.cache_repo.expiration_time = 2592000
439 487 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set
440 488 #rc_cache.cache_repo.arguments.filename = /tmp/cache_repo_db
441 489
442 490 ; alternative `cache_repo` redis backend with distributed lock
443 491 #rc_cache.cache_repo.backend = dogpile.cache.rc.redis
444 492 #rc_cache.cache_repo.expiration_time = 2592000
445 493
446 494 ; redis_expiration_time needs to be greater then expiration_time
447 495 #rc_cache.cache_repo.arguments.redis_expiration_time = 2678400
448 496
449 497 #rc_cache.cache_repo.arguments.host = localhost
450 498 #rc_cache.cache_repo.arguments.port = 6379
451 499 #rc_cache.cache_repo.arguments.db = 1
452 500 #rc_cache.cache_repo.arguments.socket_timeout = 30
453 501 ; more Redis options: https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends
454 502 #rc_cache.cache_repo.arguments.distributed_lock = true
455 503
456 504 ; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen
457 505 #rc_cache.cache_repo.arguments.lock_auto_renewal = true
458 506
459 507 ; ##############
460 508 ; BEAKER SESSION
461 509 ; ##############
462 510
463 511 ; beaker.session.type is type of storage options for the logged users sessions. Current allowed
464 512 ; types are file, ext:redis, ext:database, ext:memcached
465 513 ; Fastest ones are ext:redis and ext:database, DO NOT use memory type for session
466 514 #beaker.session.type = file
467 515 #beaker.session.data_dir = %(here)s/data/sessions
468 516
469 517 ; Redis based sessions
470 518 beaker.session.type = ext:redis
471 519 beaker.session.url = redis://redis:6379/2
472 520
473 521 ; DB based session, fast, and allows easy management over logged in users
474 522 #beaker.session.type = ext:database
475 523 #beaker.session.table_name = db_session
476 524 #beaker.session.sa.url = postgresql://postgres:secret@localhost/rhodecode
477 525 #beaker.session.sa.url = mysql://root:secret@127.0.0.1/rhodecode
478 526 #beaker.session.sa.pool_recycle = 3600
479 527 #beaker.session.sa.echo = false
480 528
481 529 beaker.session.key = rhodecode
482 530 beaker.session.secret = production-rc-uytcxaz
483 531 beaker.session.lock_dir = /data_ramdisk/lock
484 532
485 533 ; Secure encrypted cookie. Requires AES and AES python libraries
486 534 ; you must disable beaker.session.secret to use this
487 535 #beaker.session.encrypt_key = key_for_encryption
488 536 #beaker.session.validate_key = validation_key
489 537
490 538 ; Sets session as invalid (also logging out user) if it haven not been
491 539 ; accessed for given amount of time in seconds
492 540 beaker.session.timeout = 2592000
493 541 beaker.session.httponly = true
494 542
495 543 ; Path to use for the cookie. Set to prefix if you use prefix middleware
496 544 #beaker.session.cookie_path = /custom_prefix
497 545
498 546 ; Set https secure cookie
499 547 beaker.session.secure = false
500 548
501 549 ; default cookie expiration time in seconds, set to `true` to set expire
502 550 ; at browser close
503 551 #beaker.session.cookie_expires = 3600
504 552
505 553 ; #############################
506 554 ; SEARCH INDEXING CONFIGURATION
507 555 ; #############################
508 556
509 557 ; Full text search indexer is available in rhodecode-tools under
510 558 ; `rhodecode-tools index` command
511 559
512 560 ; WHOOSH Backend, doesn't require additional services to run
513 561 ; it works good with few dozen repos
514 562 search.module = rhodecode.lib.index.whoosh
515 563 search.location = %(here)s/data/index
516 564
517 565 ; ####################
518 566 ; CHANNELSTREAM CONFIG
519 567 ; ####################
520 568
521 569 ; channelstream enables persistent connections and live notification
522 570 ; in the system. It's also used by the chat system
523 571
524 572 channelstream.enabled = true
525 573
526 574 ; server address for channelstream server on the backend
527 575 channelstream.server = channelstream:9800
528 576
529 577 ; location of the channelstream server from outside world
530 578 ; use ws:// for http or wss:// for https. This address needs to be handled
531 579 ; by external HTTP server such as Nginx or Apache
532 580 ; see Nginx/Apache configuration examples in our docs
533 581 channelstream.ws_url = ws://rhodecode.yourserver.com/_channelstream
534 582 channelstream.secret = ENV_GENERATED
535 583 channelstream.history.location = /var/opt/rhodecode_data/channelstream_history
536 584
537 585 ; Internal application path that Javascript uses to connect into.
538 586 ; If you use proxy-prefix the prefix should be added before /_channelstream
539 587 channelstream.proxy_path = /_channelstream
540 588
541 589
542 590 ; ##############################
543 591 ; MAIN RHODECODE DATABASE CONFIG
544 592 ; ##############################
545 593
546 594 #sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db?timeout=30
547 595 #sqlalchemy.db1.url = postgresql://postgres:qweqwe@localhost/rhodecode
548 596 #sqlalchemy.db1.url = mysql://root:qweqwe@localhost/rhodecode?charset=utf8
549 597 ; pymysql is an alternative driver for MySQL, use in case of problems with default one
550 598 #sqlalchemy.db1.url = mysql+pymysql://root:qweqwe@localhost/rhodecode
551 599
552 600 sqlalchemy.db1.url = postgresql://postgres:qweqwe@localhost/rhodecode
553 601
554 602 ; see sqlalchemy docs for other advanced settings
555 603 ; print the sql statements to output
556 604 sqlalchemy.db1.echo = false
557 605
558 606 ; recycle the connections after this amount of seconds
559 607 sqlalchemy.db1.pool_recycle = 3600
560 608
561 609 ; the number of connections to keep open inside the connection pool.
562 610 ; 0 indicates no limit
563 611 ; the general calculus with gevent is:
564 612 ; if your system allows 500 concurrent greenlets (max_connections) that all do database access,
565 613 ; then increase pool size + max overflow so that they add up to 500.
566 614 #sqlalchemy.db1.pool_size = 5
567 615
568 616 ; The number of connections to allow in connection pool "overflow", that is
569 617 ; connections that can be opened above and beyond the pool_size setting,
570 618 ; which defaults to five.
571 619 #sqlalchemy.db1.max_overflow = 10
572 620
573 621 ; Connection check ping, used to detect broken database connections
574 622 ; could be enabled to better handle cases if MySQL has gone away errors
575 623 #sqlalchemy.db1.ping_connection = true
576 624
577 625 ; ##########
578 626 ; VCS CONFIG
579 627 ; ##########
580 628 vcs.server.enable = true
581 629 vcs.server = vcsserver:10010
582 630
583 631 ; Web server connectivity protocol, responsible for web based VCS operations
584 632 ; Available protocols are:
585 633 ; `http` - use http-rpc backend (default)
586 634 vcs.server.protocol = http
587 635
588 636 ; Push/Pull operations protocol, available options are:
589 637 ; `http` - use http-rpc backend (default)
590 638 vcs.scm_app_implementation = http
591 639
592 640 ; Push/Pull operations hooks protocol, available options are:
593 641 ; `http` - use http-rpc backend (default)
594 642 ; `celery` - use celery based hooks
595 vcs.hooks.protocol = http
643 #DEPRECATED:vcs.hooks.protocol = http
644 vcs.hooks.protocol.v2 = celery
596 645
597 646 ; Host on which this instance is listening for hooks. vcsserver will call this host to pull/push hooks so it should be
598 647 ; accessible via network.
599 648 ; Use vcs.hooks.host = "*" to bind to current hostname (for Docker)
600 649 vcs.hooks.host = *
601 650
602 651 ; Start VCSServer with this instance as a subprocess, useful for development
603 652 vcs.start_server = false
604 653
605 654 ; List of enabled VCS backends, available options are:
606 655 ; `hg` - mercurial
607 656 ; `git` - git
608 657 ; `svn` - subversion
609 658 vcs.backends = hg, git, svn
610 659
611 660 ; Wait this number of seconds before killing connection to the vcsserver
612 661 vcs.connection_timeout = 3600
613 662
614 663 ; Cache flag to cache vcsserver remote calls locally
615 664 ; It uses cache_region `cache_repo`
616 665 vcs.methods.cache = true
617 666
667 ; Filesystem location where Git lfs objects should be stored
668 vcs.git.lfs.storage_location = /var/opt/rhodecode_repo_store/.cache/git_lfs_store
669
670 ; Filesystem location where Mercurial largefile objects should be stored
671 vcs.hg.largefiles.storage_location = /var/opt/rhodecode_repo_store/.cache/hg_largefiles_store
672
618 673 ; ####################################################
619 674 ; Subversion proxy support (mod_dav_svn)
620 675 ; Maps RhodeCode repo groups into SVN paths for Apache
621 676 ; ####################################################
622 677
623 678 ; Compatibility version when creating SVN repositories. Defaults to newest version when commented out.
624 679 ; Set a numeric version for your current SVN e.g 1.8, or 1.12
625 680 ; Legacy available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible, pre-1.9-compatible
626 681 #vcs.svn.compatible_version = 1.8
627 682
628 683 ; Redis connection settings for svn integrations logic
629 684 ; This connection string needs to be the same on ce and vcsserver
630 685 vcs.svn.redis_conn = redis://redis:6379/0
631 686
632 687 ; Enable SVN proxy of requests over HTTP
633 688 vcs.svn.proxy.enabled = true
634 689
635 690 ; host to connect to running SVN subsystem
636 691 vcs.svn.proxy.host = http://svn:8090
637 692
638 693 ; Enable or disable the config file generation.
639 694 svn.proxy.generate_config = true
640 695
641 696 ; Generate config file with `SVNListParentPath` set to `On`.
642 697 svn.proxy.list_parent_path = true
643 698
644 699 ; Set location and file name of generated config file.
645 700 svn.proxy.config_file_path = /etc/rhodecode/conf/svn/mod_dav_svn.conf
646 701
647 702 ; alternative mod_dav config template. This needs to be a valid mako template
648 703 ; Example template can be found in the source code:
649 704 ; rhodecode/apps/svn_support/templates/mod-dav-svn.conf.mako
650 705 #svn.proxy.config_template = ~/.rccontrol/enterprise-1/custom_svn_conf.mako
651 706
652 707 ; Used as a prefix to the `Location` block in the generated config file.
653 708 ; In most cases it should be set to `/`.
654 709 svn.proxy.location_root = /
655 710
656 711 ; Command to reload the mod dav svn configuration on change.
657 712 ; Example: `/etc/init.d/apache2 reload` or /home/USER/apache_reload.sh
658 713 ; Make sure user who runs RhodeCode process is allowed to reload Apache
659 714 #svn.proxy.reload_cmd = /etc/init.d/apache2 reload
660 715
661 716 ; If the timeout expires before the reload command finishes, the command will
662 717 ; be killed. Setting it to zero means no timeout. Defaults to 10 seconds.
663 718 #svn.proxy.reload_timeout = 10
664 719
665 720 ; ####################
666 721 ; SSH Support Settings
667 722 ; ####################
668 723
669 724 ; Defines if a custom authorized_keys file should be created and written on
670 725 ; any change user ssh keys. Setting this to false also disables possibility
671 726 ; of adding SSH keys by users from web interface. Super admins can still
672 727 ; manage SSH Keys.
673 728 ssh.generate_authorized_keyfile = true
674 729
675 730 ; Options for ssh, default is `no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding`
676 731 # ssh.authorized_keys_ssh_opts =
677 732
678 733 ; Path to the authorized_keys file where the generate entries are placed.
679 734 ; It is possible to have multiple key files specified in `sshd_config` e.g.
680 735 ; AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys_rhodecode
681 736 ssh.authorized_keys_file_path = /etc/rhodecode/conf/ssh/authorized_keys_rhodecode
682 737
683 738 ; Command to execute the SSH wrapper. The binary is available in the
684 739 ; RhodeCode installation directory.
685 740 ; legacy: /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper
686 741 ; new rewrite: /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper-v2
687 ssh.wrapper_cmd = /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper
742 #DEPRECATED: ssh.wrapper_cmd = /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper
743 ssh.wrapper_cmd.v2 = /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper-v2
688 744
689 745 ; Allow shell when executing the ssh-wrapper command
690 746 ssh.wrapper_cmd_allow_shell = false
691 747
692 748 ; Enables logging, and detailed output send back to the client during SSH
693 749 ; operations. Useful for debugging, shouldn't be used in production.
694 750 ssh.enable_debug_logging = false
695 751
696 752 ; Paths to binary executable, by default they are the names, but we can
697 753 ; override them if we want to use a custom one
698 754 ssh.executable.hg = /usr/local/bin/rhodecode_bin/vcs_bin/hg
699 755 ssh.executable.git = /usr/local/bin/rhodecode_bin/vcs_bin/git
700 756 ssh.executable.svn = /usr/local/bin/rhodecode_bin/vcs_bin/svnserve
701 757
702 758 ; Enables SSH key generator web interface. Disabling this still allows users
703 759 ; to add their own keys.
704 760 ssh.enable_ui_key_generator = true
705 761
706 762 ; Statsd client config, this is used to send metrics to statsd
707 763 ; We recommend setting statsd_exported and scrape them using Prometheus
708 764 #statsd.enabled = false
709 765 #statsd.statsd_host = 0.0.0.0
710 766 #statsd.statsd_port = 8125
711 767 #statsd.statsd_prefix =
712 768 #statsd.statsd_ipv6 = false
713 769
714 770 ; configure logging automatically at server startup set to false
715 771 ; to use the below custom logging config.
716 772 ; RC_LOGGING_FORMATTER
717 773 ; RC_LOGGING_LEVEL
718 774 ; env variables can control the settings for logging in case of autoconfigure
719 775
720 776 #logging.autoconfigure = true
721 777
722 778 ; specify your own custom logging config file to configure logging
723 779 #logging.logging_conf_file = /path/to/custom_logging.ini
724 780
725 781 ; Dummy marker to add new entries after.
726 782 ; Add any custom entries below. Please don't remove this marker.
727 783 custom.conf = 1
728 784
729 785
730 786 ; #####################
731 787 ; LOGGING CONFIGURATION
732 788 ; #####################
733 789
734 790 [loggers]
735 791 keys = root, sqlalchemy, beaker, celery, rhodecode, ssh_wrapper
736 792
737 793 [handlers]
738 794 keys = console, console_sql
739 795
740 796 [formatters]
741 797 keys = generic, json, color_formatter, color_formatter_sql
742 798
743 799 ; #######
744 800 ; LOGGERS
745 801 ; #######
746 802 [logger_root]
747 803 level = NOTSET
748 804 handlers = console
749 805
750 806 [logger_sqlalchemy]
751 807 level = INFO
752 808 handlers = console_sql
753 809 qualname = sqlalchemy.engine
754 810 propagate = 0
755 811
756 812 [logger_beaker]
757 813 level = DEBUG
758 814 handlers =
759 815 qualname = beaker.container
760 816 propagate = 1
761 817
762 818 [logger_rhodecode]
763 819 level = DEBUG
764 820 handlers =
765 821 qualname = rhodecode
766 822 propagate = 1
767 823
768 824 [logger_ssh_wrapper]
769 825 level = DEBUG
770 826 handlers =
771 827 qualname = ssh_wrapper
772 828 propagate = 1
773 829
774 830 [logger_celery]
775 831 level = DEBUG
776 832 handlers =
777 833 qualname = celery
778 834
779 835
780 836 ; ########
781 837 ; HANDLERS
782 838 ; ########
783 839
784 840 [handler_console]
785 841 class = StreamHandler
786 842 args = (sys.stderr, )
787 843 level = INFO
788 844 ; To enable JSON formatted logs replace 'generic/color_formatter' with 'json'
789 845 ; This allows sending properly formatted logs to grafana loki or elasticsearch
790 846 formatter = generic
791 847
792 848 [handler_console_sql]
793 849 ; "level = DEBUG" logs SQL queries and results.
794 850 ; "level = INFO" logs SQL queries.
795 851 ; "level = WARN" logs neither. (Recommended for production systems.)
796 852 class = StreamHandler
797 853 args = (sys.stderr, )
798 854 level = WARN
799 855 ; To enable JSON formatted logs replace 'generic/color_formatter_sql' with 'json'
800 856 ; This allows sending properly formatted logs to grafana loki or elasticsearch
801 857 formatter = generic
802 858
803 859 ; ##########
804 860 ; FORMATTERS
805 861 ; ##########
806 862
807 863 [formatter_generic]
808 864 class = rhodecode.lib.logging_formatter.ExceptionAwareFormatter
809 865 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
810 866 datefmt = %Y-%m-%d %H:%M:%S
811 867
812 868 [formatter_color_formatter]
813 869 class = rhodecode.lib.logging_formatter.ColorFormatter
814 870 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
815 871 datefmt = %Y-%m-%d %H:%M:%S
816 872
817 873 [formatter_color_formatter_sql]
818 874 class = rhodecode.lib.logging_formatter.ColorFormatterSql
819 875 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
820 876 datefmt = %Y-%m-%d %H:%M:%S
821 877
822 878 [formatter_json]
823 879 format = %(timestamp)s %(levelname)s %(name)s %(message)s %(req_id)s
824 880 class = rhodecode.lib._vendor.jsonlogger.JsonFormatter
@@ -1,33 +1,39 b''
1 1 FROM python:3.12.0-bullseye
2 2
3 3 WORKDIR /project
4 4
5 5 RUN apt-get update \
6 6 && apt-get install --no-install-recommends --yes \
7 7 curl \
8 8 zip \
9 9 graphviz \
10 10 dvipng \
11 11 imagemagick \
12 12 make \
13 13 latexmk \
14 14 texlive-latex-recommended \
15 15 texlive-latex-extra \
16 16 texlive-xetex \
17 17 fonts-freefont-otf \
18 18 texlive-fonts-recommended \
19 19 texlive-lang-greek \
20 20 tex-gyre \
21 21 && apt-get autoremove \
22 22 && apt-get clean \
23 23 && rm -rf /var/lib/apt/lists/*
24 24
25 RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && \
26 unzip awscliv2.zip && \
27 ./aws/install && \
28 rm -rf ./aws && \
29 rm awscliv2.zip
30
25 31 RUN \
26 32 python3 -m pip install --no-cache-dir --upgrade pip && \
27 33 python3 -m pip install --no-cache-dir Sphinx Pillow
28 34
29 35 ADD requirements_docs.txt /project
30 36 RUN \
31 37 python3 -m pip install -r requirements_docs.txt
32 38
33 39 CMD ["sphinx-build", "-M", "html", ".", "_build"]
@@ -1,172 +1,168 b''
1 1 .. _system-overview-ref:
2 2
3 3 System Overview
4 4 ===============
5 5
6 6 Latest Version
7 7 --------------
8 8
9 9 * |release| on Unix and Windows systems.
10 10
11 11 System Architecture
12 12 -------------------
13 13
14 14 The following diagram shows a typical production architecture.
15 15
16 16 .. image:: ../images/architecture-diagram.png
17 17 :align: center
18 18
19 19 Supported Operating Systems
20 20 ---------------------------
21 21
22 22 Linux
23 23 ^^^^^
24 24
25 25 * Ubuntu 14.04+
26 26 * CentOS 6.2, 7 and 8
27 27 * RHEL 6.2, 7 and 8
28 28 * Debian 7.8
29 29 * RedHat Fedora
30 30 * Arch Linux
31 31 * SUSE Linux
32 32
33 33 Windows
34 34 ^^^^^^^
35 35
36 36 * Windows Vista Ultimate 64bit
37 37 * Windows 7 Ultimate 64bit
38 38 * Windows 8 Professional 64bit
39 39 * Windows 8.1 Enterprise 64bit
40 40 * Windows Server 2008 64bit
41 41 * Windows Server 2008-R2 64bit
42 42 * Windows Server 2012 64bit
43 43
44 44 Supported Databases
45 45 -------------------
46 46
47 47 * SQLite
48 48 * MySQL
49 49 * MariaDB
50 50 * PostgreSQL
51 51
52 52 Supported Browsers
53 53 ------------------
54 54
55 55 * Chrome
56 56 * Safari
57 57 * Firefox
58 58 * Internet Explorer 10 & 11
59 59
60 60 System Requirements
61 61 -------------------
62 62
63 63 |RCE| performs best on machines with ultra-fast hard disks. Generally disk
64 64 performance is more important than CPU performance. In a corporate production
65 65 environment handling 1000s of users and |repos| you should deploy on a 12+
66 66 core 64GB RAM server. In short, the more RAM the better.
67 67
68 68
69 69 For example:
70 70
71 71 - for team of 1 - 5 active users you can run on 1GB RAM machine with 1CPU
72 72 - above 250 active users, |RCE| needs at least 8GB of memory.
73 73 Number of CPUs is less important, but recommended to have at least 2-3 CPUs
74 74
75 75
76 76 .. _config-rce-files:
77 77
78 78 Configuration Files
79 79 -------------------
80 80
81 81 * :file:`config/_shared/rhodecode.ini`
82 82 * :file:`/home/{user}/.rccontrol/{instance-id}/search_mapping.ini`
83 83 * :file:`/home/{user}/.rccontrol/{vcsserver-id}/vcsserver.ini`
84 84 * :file:`/home/{user}/.rccontrol/supervisor/supervisord.ini`
85 85 * :file:`/home/{user}/.rccontrol.ini`
86 86 * :file:`/home/{user}/.rhoderc`
87 87 * :file:`/home/{user}/.rccontrol/cache/MANIFEST`
88 88
89 89 For more information, see the :ref:`config-files` section.
90 90
91 91 Log Files
92 92 ---------
93 93
94 94 * :file:`/home/{user}/.rccontrol/{instance-id}/enterprise.log`
95 95 * :file:`/home/{user}/.rccontrol/{vcsserver-id}/vcsserver.log`
96 96 * :file:`/home/{user}/.rccontrol/supervisor/supervisord.log`
97 97 * :file:`/tmp/rccontrol.log`
98 98 * :file:`/tmp/rhodecode_tools.log`
99 99
100 100 Storage Files
101 101 -------------
102 102
103 103 * :file:`/home/{user}/.rccontrol/{instance-id}/data/index/{index-file.toc}`
104 104 * :file:`/home/{user}/repos/.rc_gist_store`
105 105 * :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.db`
106 106 * :file:`/opt/rhodecode/store/{unique-hash}`
107 107
108 108 Default Repositories Location
109 109 -----------------------------
110 110
111 111 * :file:`/home/{user}/repos`
112 112
113 113 Connection Methods
114 114 ------------------
115 115
116 116 * HTTPS
117 117 * SSH
118 118 * |RCE| API
119 119
120 120 Internationalization Support
121 121 ----------------------------
122 122
123 123 Currently available in the following languages, see `Transifex`_ for the
124 124 latest details. If you want a new language added, please contact us. To
125 125 configure your language settings, see the :ref:`set-lang` section.
126 126
127 127 .. hlist::
128 128
129 129 * Belorussian
130 130 * Chinese
131 131 * French
132 132 * German
133 133 * Italian
134 134 * Japanese
135 135 * Portuguese
136 136 * Polish
137 137 * Russian
138 138 * Spanish
139 139
140 140 Licencing Information
141 141 ---------------------
142 142
143 143 * See licencing information `here`_
144 144
145 145 Peer-to-peer Failover Support
146 146 -----------------------------
147 147
148 148 * Yes
149 149
150 Additional Binaries
151 -------------------
152
153 * Yes, see :ref:`rhodecode-nix-ref` for full details.
154 150
155 151 Remote Connectivity
156 152 -------------------
157 153
158 154 * Available
159 155
160 156 Executable Files
161 157 ----------------
162 158
163 159 Windows: :file:`RhodeCode-installer-{version}.exe`
164 160
165 161 Deprecated Support
166 162 ------------------
167 163
168 164 - Internet Explorer 8 support deprecated since version 3.7.0.
169 165 - Internet Explorer 9 support deprecated since version 3.8.0.
170 166
171 167 .. _here: https://rhodecode.com/licenses/
172 168 .. _Transifex: https://explore.transifex.com/rhodecode/RhodeCode/
@@ -1,88 +1,90 b''
1 1 .. _auth-saml-bulk-enroll-users-ref:
2 2
3 3
4 4 Bulk enroll multiple existing users
5 5 -----------------------------------
6 6
7 7
8 8 RhodeCode Supports standard SAML 2.0 SSO for the web-application part.
9 9 Below is an example how to enroll list of all or some users to use SAML authentication.
10 10 This method simply enables SAML authentication for many users at once.
11 11
12 12
13 13 From the server RhodeCode Enterprise is running run ishell on the instance which we
14 14 want to apply the SAML migration::
15 15
16 rccontrol ishell enterprise-1
16 ./rcstack cli ishell
17 17
18 18 Follow these steps to enable SAML authentication for multiple users.
19 19
20 20
21 21 1) Create a user_id => attribute mapping
22 22
23 23
24 24 `saml2user` is a mapping of external ID from SAML provider such as OneLogin, DuoSecurity, Google.
25 25 This mapping consists of local rhodecode user_id mapped to set of required attributes needed to bind SAML
26 26 account to internal rhodecode user.
27 27 For example, 123 is local rhodecode user_id, and '48253211' is OneLogin ID.
28 28 For other providers you'd have to figure out what would be the user-id, sometimes it's the email, i.e for Google
29 29 The most important this id needs to be unique for each user.
30 30
31 31 .. code-block:: python
32 32
33 33 In [1]: saml2user = {
34 34 ...: # OneLogin, uses externalID available to read from in the UI
35 35 ...: 123: {'id': '48253211'},
36 36 ...: # for Google/DuoSecurity email is also an option for unique ID
37 37 ...: 124: {'id': 'email@domain.com'},
38 38 ...: }
39 39
40 40
41 41 2) Import the plugin you want to run migration for.
42 42
43 43 From available options pick only one and run the `import` statement
44 44
45 45 .. code-block:: python
46 46
47 47 # for Duo Security
48 48 In [2]: from rc_auth_plugins.auth_duo_security import RhodeCodeAuthPlugin
49 # for Azure Entra
50 In [2]: from rc_auth_plugins.auth_azure import RhodeCodeAuthPlugin
49 51 # for OneLogin
50 52 In [2]: from rc_auth_plugins.auth_onelogin import RhodeCodeAuthPlugin
51 53 # generic SAML plugin
52 54 In [2]: from rc_auth_plugins.auth_saml import RhodeCodeAuthPlugin
53 55
54 56 3) Run the migration based on saml2user mapping.
55 57
56 58 Enter in the ishell prompt
57 59
58 60 .. code-block:: python
59 61
60 62 In [3]: for user in User.get_all():
61 63 ...: existing_identity = ExternalIdentity().query().filter(ExternalIdentity.local_user_id == user.user_id).scalar()
62 64 ...: attrs = saml2user.get(user.user_id)
63 65 ...: provider = RhodeCodeAuthPlugin.uid
64 66 ...: if existing_identity:
65 ...: print('Identity for user `{}` already exists, skipping'.format(user.username))
67 ...: print(f'Identity for user `{user.username}` already exists, skipping')
66 68 ...: continue
67 69 ...: if attrs:
68 70 ...: external_id = attrs['id']
69 71 ...: new_external_identity = ExternalIdentity()
70 72 ...: new_external_identity.external_id = external_id
71 ...: new_external_identity.external_username = '{}-saml-{}'.format(user.username, user.user_id)
73 ...: new_external_identity.external_username = f'{user.username}-saml-{user.user_id}'
72 74 ...: new_external_identity.provider_name = provider
73 75 ...: new_external_identity.local_user_id = user.user_id
74 76 ...: new_external_identity.access_token = ''
75 77 ...: new_external_identity.token_secret = ''
76 78 ...: new_external_identity.alt_token = ''
77 79 ...: Session().add(ex_identity)
78 80 ...: Session().commit()
79 ...: print('Set user `{}` external identity bound to ExternalID:{}'.format(user.username, external_id))
81 ...: print(f'Set user `{user.username}` external identity bound to ExternalID:{external_id}')
80 82
81 83 .. note::
82 84
83 85 saml2user can be really big and hard to maintain in ishell. It's also possible
84 86 to load it as a JSON file prepared before and stored on disk. To do so run::
85 87
86 88 import json
87 89 saml2user = json.loads(open('/path/to/saml2user.json','rb').read())
88 90
@@ -1,105 +1,161 b''
1 1 .. _config-saml-duosecurity-ref:
2 2
3 3
4 4 SAML 2.0 with Duo Security
5 5 --------------------------
6 6
7 7 **This plugin is available only in EE Edition.**
8 8
9 9 |RCE| supports SAML 2.0 Authentication with Duo Security provider. This allows
10 10 users to log-in to RhodeCode via SSO mechanism of external identity provider
11 11 such as Duo. The login can be triggered either by the external IDP, or internally
12 12 by clicking specific authentication button on the log-in page.
13 13
14 14
15 15 Configuration steps
16 16 ^^^^^^^^^^^^^^^^^^^
17 17
18 18 To configure Duo Security SAML authentication, use the following steps:
19 19
20 20 1. From the |RCE| interface, select
21 21 :menuselection:`Admin --> Authentication`
22 22 2. Activate the `Duo Security` plugin and select :guilabel:`Save`
23 23 3. Go to newly available menu option called `Duo Security` on the left side.
24 24 4. Check the `enabled` check box in the plugin configuration section,
25 25 and fill in the required SAML information and :guilabel:`Save`, for more details,
26 26 see :ref:`config-saml-duosecurity`
27 27
28 28
29 29 .. _config-saml-duosecurity:
30 30
31 31
32 32 Example SAML Duo Security configuration
33 33 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
34 34
35 Example configuration for SAML 2.0 with Duo Security provider::
35 Example configuration for SAML 2.0 with Duo Security provider
36
37
38 Enabled
39 `True`:
36 40
37 *option*: `enabled` => `True`
38 # Enable or disable this authentication plugin.
41 .. note::
42 Enable or disable this authentication plugin.
43
44
45 Auth Cache TTL
46 `30`:
39 47
40 *option*: `cache_ttl` => `0`
41 # Amount of seconds to cache the authentication and permissions check response call for this plugin.
42 # Useful for expensive calls like LDAP to improve the performance of the system (0 means disabled).
48 .. note::
49 Amount of seconds to cache the authentication and permissions check response call for this plugin.
50 Useful for expensive calls like LDAP to improve the performance of the system (0 means disabled).
51
52 Debug
53 `True`:
43 54
44 *option*: `debug` => `True`
45 # Enable or disable debug mode that shows SAML errors in the RhodeCode logs.
55 .. note::
56 Enable or disable debug mode that shows SAML errors in the RhodeCode logs.
57
58
59 Auth button name
60 `Azure Entra ID`:
46 61
47 *option*: `entity_id` => `http://rc-app.com/dag/saml2/idp/metadata.php`
48 # Identity Provider entity/metadata URI.
49 # E.g. https://duo-gateway.com/dag/saml2/idp/metadata.php
62 .. note::
63 Alternative authentication display name. E.g AzureAuth, CorporateID etc.
64
65
66 Entity ID
67 `https://my-duo-gateway.com/dag/saml2/idp/metadata.php`:
68
69 .. note::
70 Identity Provider entity/metadata URI.
71 E.g. https://duo-gateway.com/dag/saml2/idp/metadata.php
72
73 SSO URL
74 `https://duo-gateway.com/dag/saml2/idp/SSOService.php?spentityid=<metadata_entity_id>`:
50 75
51 *option*: `sso_service_url` => `http://rc-app.com/dag/saml2/idp/SSOService.php?spentityid=http://rc.local.pl/_admin/auth/duosecurity/saml-metadata`
52 # SSO (SingleSignOn) endpoint URL of the IdP. This can be used to initialize login
53 # E.g. https://duo-gateway.com/dag/saml2/idp/SSOService.php?spentityid=<metadata_entity_id>
76 .. note::
77 SSO (SingleSignOn) endpoint URL of the IdP. This can be used to initialize login, Known also as Login URL
78 E.g. http://rc-app.com/dag/saml2/idp/SSOService.php?spentityid=https://docker-dev/_admin/auth/duosecurity/saml-metadata
79
80 SLO URL
81 `https://duo-gateway.com/dag/saml2/idp/SingleLogoutService.php?ReturnTo=<return_url>`:
54 82
55 *option*: `slo_service_url` => `http://rc-app.com/dag/saml2/idp/SingleLogoutService.php?ReturnTo=http://rc-app.com/dag/module.php/duosecurity/logout.php`
56 # SLO (SingleLogout) endpoint URL of the IdP.
57 # E.g. https://duo-gateway.com/dag/saml2/idp/SingleLogoutService.php?ReturnTo=http://duo-gateway.com/_admin/saml/sign-out-endpoint
83 .. note::
84 SLO (SingleLogout) endpoint URL of the IdP. , Known also as Logout URL
85 E.g. http://rc-app.com/dag/saml2/idp/SingleLogoutService.php?ReturnTo=https://docker-dev/_admin/auth/duosecurity/saml-sign-out-endpoint
58 86
59 *option*: `x509cert` => `<CERTIFICATE_STRING>`
60 # Identity provider public x509 certificate. It will be converted to single-line format without headers
87 x509cert
88 `<CERTIFICATE_STRING>`:
61 89
62 *option*: `name_id_format` => `sha-1`
63 # The format that specifies how the NameID is sent to the service provider.
90 .. note::
91 Identity provider public x509 certificate. It will be converted to single-line format without headers.
92 Download the raw base64 encoded certificate from the Identity provider and paste it here.
93
94 SAML Signature
95 `sha-256`:
96
97 .. note::
98 Type of Algorithm to use for verification of SAML signature on Identity provider side.
99
100 SAML Digest
101 `sha-256`:
64 102
65 *option*: `signature_algo` => `sha-256`
66 # Type of Algorithm to use for verification of SAML signature on Identity provider side
103 .. note::
104 Type of Algorithm to use for verification of SAML digest on Identity provider side.
105
106 Service Provider Cert Dir
107 `/etc/rhodecode/conf/saml_ssl/`:
67 108
68 *option*: `digest_algo` => `sha-256`
69 # Type of Algorithm to use for verification of SAML digest on Identity provider side
109 .. note::
110 Optional directory to store service provider certificate and private keys.
111 Expected certs for the SP should be stored in this folder as:
112
113 * sp.key Private Key
114 * sp.crt Public cert
115 * sp_new.crt Future Public cert
116
117 Also you can use other cert to sign the metadata of the SP using the:
70 118
71 *option*: `cert_dir` => `/etc/saml/`
72 # Optional directory to store service provider certificate and private keys.
73 # Expected certs for the SP should be stored in this folder as:
74 # * sp.key Private Key
75 # * sp.crt Public cert
76 # * sp_new.crt Future Public cert
77 #
78 # Also you can use other cert to sign the metadata of the SP using the:
79 # * metadata.key
80 # * metadata.crt
119 * metadata.key
120 * metadata.crt
121
122 Expected NameID Format
123 `nameid-format:emailAddress`:
124
125 .. note::
126 The format that specifies how the NameID is sent to the service provider.
127
128 User ID Attribute
129 `PersonImmutableID`:
81 130
82 *option*: `user_id_attribute` => `PersonImmutableID`
83 # User ID Attribute name. This defines which attribute in SAML response will be used to link accounts via unique id.
84 # Ensure this is returned from DuoSecurity for example via duo_username
131 .. note::
132 User ID Attribute name. This defines which attribute in SAML response will be used to link accounts via unique id.
133 Ensure this is returned from DuoSecurity for example via duo_username.
134
135 Username Attribute
136 `User.username`:
85 137
86 *option*: `username_attribute` => `User.username`
87 # Username Attribute name. This defines which attribute in SAML response will map to an username.
138 .. note::
139 Username Attribute name. This defines which attribute in SAML response will map to a username.
88 140
89 *option*: `email_attribute` => `User.email`
90 # Email Attribute name. This defines which attribute in SAML response will map to an email address.
141 Email Attribute
142 `User.email`:
143
144 .. note::
145 Email Attribute name. This defines which attribute in SAML response will map to an email address.
146
91 147
92 148
93 149 Below is example setup from DUO Administration page that can be used with above config.
94 150
95 151 .. image:: ../images/saml-duosecurity-service-provider-example.png
96 152 :alt: DUO Security SAML setup example
97 153 :scale: 50 %
98 154
99 155
100 156 Below is an example attribute mapping set for IDP provider required by the above config.
101 157
102 158
103 159 .. image:: ../images/saml-duosecurity-attributes-example.png
104 160 :alt: DUO Security SAML setup example
105 161 :scale: 50 % No newline at end of file
@@ -1,19 +1,20 b''
1 1 .. _config-saml-generic-ref:
2 2
3 3
4 4 SAML 2.0 Authentication
5 5 -----------------------
6 6
7 7
8 8 **This plugin is available only in EE Edition.**
9 9
10 10 RhodeCode Supports standard SAML 2.0 SSO for the web-application part.
11 11
12 12 Please check for reference two example providers:
13 13
14 14 .. toctree::
15 15
16 16 auth-saml-duosecurity
17 17 auth-saml-onelogin
18 auth-saml-azure
18 19 auth-saml-bulk-enroll-users
19 20
@@ -1,106 +1,161 b''
1 1 .. _config-saml-onelogin-ref:
2 2
3 3
4 4 SAML 2.0 with One Login
5 5 -----------------------
6 6
7 7 **This plugin is available only in EE Edition.**
8 8
9 9 |RCE| supports SAML 2.0 Authentication with OneLogin provider. This allows
10 10 users to log-in to RhodeCode via SSO mechanism of external identity provider
11 11 such as OneLogin. The login can be triggered either by the external IDP, or internally
12 12 by clicking specific authentication button on the log-in page.
13 13
14 14
15 15 Configuration steps
16 16 ^^^^^^^^^^^^^^^^^^^
17 17
18 18 To configure OneLogin SAML authentication, use the following steps:
19 19
20 20 1. From the |RCE| interface, select
21 21 :menuselection:`Admin --> Authentication`
22 22 2. Activate the `OneLogin` plugin and select :guilabel:`Save`
23 23 3. Go to newly available menu option called `OneLogin` on the left side.
24 24 4. Check the `enabled` check box in the plugin configuration section,
25 25 and fill in the required SAML information and :guilabel:`Save`, for more details,
26 26 see :ref:`config-saml-onelogin`
27 27
28 28
29 29 .. _config-saml-onelogin:
30 30
31 31
32 32 Example SAML OneLogin configuration
33 33 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
34 34
35 Example configuration for SAML 2.0 with OneLogin provider::
35 Example configuration for SAML 2.0 with OneLogin provider
36
37
38 Enabled
39 `True`:
36 40
37 *option*: `enabled` => `True`
38 # Enable or disable this authentication plugin.
41 .. note::
42 Enable or disable this authentication plugin.
43
44
45 Auth Cache TTL
46 `30`:
39 47
40 *option*: `cache_ttl` => `0`
41 # Amount of seconds to cache the authentication and permissions check response call for this plugin.
42 # Useful for expensive calls like LDAP to improve the performance of the system (0 means disabled).
48 .. note::
49 Amount of seconds to cache the authentication and permissions check response call for this plugin.
50 Useful for expensive calls like LDAP to improve the performance of the system (0 means disabled).
51
52 Debug
53 `True`:
43 54
44 *option*: `debug` => `True`
45 # Enable or disable debug mode that shows SAML errors in the RhodeCode logs.
55 .. note::
56 Enable or disable debug mode that shows SAML errors in the RhodeCode logs.
57
58
59 Auth button name
60 `Azure Entra ID`:
46 61
47 *option*: `entity_id` => `https://app.onelogin.com/saml/metadata/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`
48 # Identity Provider entity/metadata URI.
49 # E.g. https://app.onelogin.com/saml/metadata/<onelogin_connector_id>
62 .. note::
63 Alternative authentication display name. E.g AzureAuth, CorporateID etc.
64
65
66 Entity ID
67 `https://app.onelogin.com/saml/metadata/<onelogin_connector_id>`:
68
69 .. note::
70 Identity Provider entity/metadata URI.
71 E.g. https://app.onelogin.com/saml/metadata/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
72
73 SSO URL
74 `https://app.onelogin.com/trust/saml2/http-post/sso/<onelogin_connector_id>`:
50 75
51 *option*: `sso_service_url` => `https://customer-domain.onelogin.com/trust/saml2/http-post/sso/xxxxxx`
52 # SSO (SingleSignOn) endpoint URL of the IdP. This can be used to initialize login
53 # E.g. https://app.onelogin.com/trust/saml2/http-post/sso/<onelogin_connector_id>
76 .. note::
77 SSO (SingleSignOn) endpoint URL of the IdP. This can be used to initialize login, Known also as Login URL
78 E.g. https://app.onelogin.com/trust/saml2/http-post/sso/<onelogin_connector_id>
79
80 SLO URL
81 `https://app.onelogin.com/trust/saml2/http-redirect/slo/<onelogin_connector_id>`:
54 82
55 *option*: `slo_service_url` => `https://customer-domain.onelogin.com/trust/saml2/http-redirect/slo/xxxxxx`
56 # SLO (SingleLogout) endpoint URL of the IdP.
57 # E.g. https://app.onelogin.com/trust/saml2/http-redirect/slo/<onelogin_connector_id>
83 .. note::
84 SLO (SingleLogout) endpoint URL of the IdP. , Known also as Logout URL
85 E.g. https://app.onelogin.com/trust/saml2/http-redirect/slo/<onelogin_connector_id>
58 86
59 *option*: `x509cert` => `<CERTIFICATE_STRING>`
60 # Identity provider public x509 certificate. It will be converted to single-line format without headers
87 x509cert
88 `<CERTIFICATE_STRING>`:
61 89
62 *option*: `name_id_format` => `sha-1`
63 # The format that specifies how the NameID is sent to the service provider.
90 .. note::
91 Identity provider public x509 certificate. It will be converted to single-line format without headers.
92 Download the raw base64 encoded certificate from the Identity provider and paste it here.
93
94 SAML Signature
95 `sha-256`:
96
97 .. note::
98 Type of Algorithm to use for verification of SAML signature on Identity provider side.
99
100 SAML Digest
101 `sha-256`:
64 102
65 *option*: `signature_algo` => `sha-256`
66 # Type of Algorithm to use for verification of SAML signature on Identity provider side
103 .. note::
104 Type of Algorithm to use for verification of SAML digest on Identity provider side.
105
106 Service Provider Cert Dir
107 `/etc/rhodecode/conf/saml_ssl/`:
67 108
68 *option*: `digest_algo` => `sha-256`
69 # Type of Algorithm to use for verification of SAML digest on Identity provider side
109 .. note::
110 Optional directory to store service provider certificate and private keys.
111 Expected certs for the SP should be stored in this folder as:
112
113 * sp.key Private Key
114 * sp.crt Public cert
115 * sp_new.crt Future Public cert
70 116
71 *option*: `cert_dir` => `/etc/saml/`
72 # Optional directory to store service provider certificate and private keys.
73 # Expected certs for the SP should be stored in this folder as:
74 # * sp.key Private Key
75 # * sp.crt Public cert
76 # * sp_new.crt Future Public cert
77 #
78 # Also you can use other cert to sign the metadata of the SP using the:
79 # * metadata.key
80 # * metadata.crt
117 Also you can use other cert to sign the metadata of the SP using the:
118
119 * metadata.key
120 * metadata.crt
121
122 Expected NameID Format
123 `nameid-format:emailAddress`:
124
125 .. note::
126 The format that specifies how the NameID is sent to the service provider.
127
128 User ID Attribute
129 `PersonImmutableID`:
81 130
82 *option*: `user_id_attribute` => `PersonImmutableID`
83 # User ID Attribute name. This defines which attribute in SAML response will be used to link accounts via unique id.
84 # Ensure this is returned from OneLogin for example via Internal ID
131 .. note::
132 User ID Attribute name. This defines which attribute in SAML response will be used to link accounts via unique id.
133 Ensure this is returned from DuoSecurity for example via duo_username.
134
135 Username Attribute
136 `User.username`:
85 137
86 *option*: `username_attribute` => `User.username`
87 # Username Attribute name. This defines which attribute in SAML response will map to an username.
138 .. note::
139 Username Attribute name. This defines which attribute in SAML response will map to a username.
88 140
89 *option*: `email_attribute` => `User.email`
90 # Email Attribute name. This defines which attribute in SAML response will map to an email address.
141 Email Attribute
142 `User.email`:
143
144 .. note::
145 Email Attribute name. This defines which attribute in SAML response will map to an email address.
91 146
92 147
93 148
94 149 Below is example setup that can be used with OneLogin SAML authentication that can be used with above config..
95 150
96 151 .. image:: ../images/saml-onelogin-config-example.png
97 152 :alt: OneLogin SAML setup example
98 153 :scale: 50 %
99 154
100 155
101 156 Below is an example attribute mapping set for IDP provider required by the above config.
102 157
103 158
104 159 .. image:: ../images/saml-onelogin-attributes-example.png
105 160 :alt: OneLogin SAML setup example
106 161 :scale: 50 % No newline at end of file
@@ -1,34 +1,35 b''
1 1 .. _authentication-ref:
2 2
3 3 Authentication Options
4 4 ======================
5 5
6 6 |RCE| provides a built in authentication against its own database. This is
7 7 implemented using ``RhodeCode Internal`` plugin. This plugin is enabled by default.
8 8 Additionally, |RCE| provides a Pluggable Authentication System. This gives the
9 9 administrator greater control over how users authenticate with the system.
10 10
11 11 .. important::
12 12
13 13 You can disable the built in |RCE| authentication plugin
14 14 ``RhodeCode Internal`` and force all authentication to go
15 15 through your authentication plugin of choice e.g LDAP only.
16 16 However, if you do this, and your external authentication tools fails,
17 17 accessing |RCE| will be blocked unless a fallback plugin is
18 18 enabled via :file: rhodecode.ini
19 19
20 20
21 21 |RCE| comes with the following user authentication management plugins:
22 22
23 23
24 24 .. toctree::
25 25
26 26 auth-token
27 27 auth-ldap
28 28 auth-ldap-groups
29 29 auth-saml-generic
30 30 auth-saml-onelogin
31 31 auth-saml-duosecurity
32 auth-saml-azure
32 33 auth-crowd
33 34 auth-pam
34 35 ssh-connection
@@ -1,243 +1,14 b''
1 1 .. _dev-setup:
2 2
3 3 ===================
4 4 Development setup
5 5 ===================
6 6
7
8 RhodeCode Enterprise runs inside a Nix managed environment. This ensures build
9 environment dependencies are correctly declared and installed during setup.
10 It also enables atomic upgrades, rollbacks, and multiple instances of RhodeCode
11 Enterprise running with isolation.
12
13 To set up RhodeCode Enterprise inside the Nix environment, use the following steps:
14
15
16
17 Setup Nix Package Manager
18 -------------------------
19
20 To install the Nix Package Manager, please run::
21
22 $ curl https://releases.nixos.org/nix/nix-2.3.4/install | sh
23
24 or go to https://nixos.org/nix/ and follow the installation instructions.
25 Once this is correctly set up on your system, you should be able to use the
26 following commands:
27
28 * `nix-env`
29
30 * `nix-shell`
31
32
33 .. tip::
34
35 Update your channels frequently by running ``nix-channel --update``.
36
37 .. note::
38
39 To uninstall nix run the following:
40
41 remove the . "$HOME/.nix-profile/etc/profile.d/nix.sh" line in your ~/.profile or ~/.bash_profile
42 rm -rf $HOME/{.nix-channels,.nix-defexpr,.nix-profile,.config/nixpkgs}
43 sudo rm -rf /nix
44
45 Switch nix to the latest STABLE channel
46 ---------------------------------------
47
48 run::
49
50 nix-channel --add https://nixos.org/channels/nixos-20.03 nixpkgs
51
52 Followed by::
53
54 nix-channel --update
55 nix-env -i nix-2.3.4
56
57
58 Install required binaries
59 -------------------------
60
61 We need some handy tools first.
62
63 run::
64
65 nix-env -i nix-prefetch-hg
66 nix-env -i nix-prefetch-git
67
68
69 Speed up JS build by installing PhantomJS
70 -----------------------------------------
71
72 PhantomJS will be downloaded each time nix-shell is invoked. To speed this by
73 setting already downloaded version do this::
74
75 nix-env -i phantomjs-2.1.1
76
77 # and set nix bin path
78 export PATH=$PATH:~/.nix-profile/bin
79
80
81 Clone the required repositories
82 -------------------------------
83
84 After Nix is set up, clone the RhodeCode Enterprise Community Edition and
85 RhodeCode VCSServer repositories into the same directory.
86 RhodeCode currently is using Mercurial Version Control System, please make sure
87 you have it installed before continuing.
88
89 To obtain the required sources, use the following commands::
90
91 mkdir rhodecode-develop && cd rhodecode-develop
92 hg clone -u default https://code.rhodecode.com/rhodecode-enterprise-ce
93 hg clone -u default https://code.rhodecode.com/rhodecode-vcsserver
94
95 .. note::
96
97 If you cannot clone the repository, please contact us via support@rhodecode.com
98
99
100 Install some required libraries
101 -------------------------------
102
103 There are some required drivers and dev libraries that we need to install to
104 test RhodeCode under different types of databases. For example in Ubuntu we
105 need to install the following.
106
107 required libraries::
108
109 # svn related
110 sudo apt-get install libapr1-dev libaprutil1-dev
111 sudo apt-get install libsvn-dev
112 # libcurl required too
113 sudo apt-get install libcurl4-openssl-dev
114 # mysql/pg server for development, optional
115 sudo apt-get install mysql-server libmysqlclient-dev
116 sudo apt-get install postgresql postgresql-contrib libpq-dev
117
118
119
120 Enter the Development Shell
121 ---------------------------
122
123 The final step is to start the development shells. To do this, run the
124 following command from inside the cloned repository::
125
126 # first, the vcsserver
127 cd ~/rhodecode-vcsserver
128 nix-shell
129
130 # then enterprise sources
131 cd ~/rhodecode-enterprise-ce
132 nix-shell
133
134 .. note::
135
136 On the first run, this will take a while to download and optionally compile
137 a few things. The following runs will be faster. The development shell works
138 fine on both MacOS and Linux platforms.
139
140
141 Create config.nix for development
142 ---------------------------------
143
144 In order to run proper tests and setup linking across projects, a config.nix
145 file needs to be setup::
146
147 # create config
148 mkdir -p ~/.nixpkgs
149 touch ~/.nixpkgs/config.nix
150
151 # put the below content into the ~/.nixpkgs/config.nix file
152 # adjusts, the path to where you cloned your repositories.
153
154 {
155 rc = {
156 sources = {
157 rhodecode-vcsserver = "/home/dev/rhodecode-vcsserver";
158 rhodecode-enterprise-ce = "/home/dev/rhodecode-enterprise-ce";
159 rhodecode-enterprise-ee = "/home/dev/rhodecode-enterprise-ee";
160 };
161 };
162 }
163
164
165
166 Creating a Development Configuration
167 ------------------------------------
168
169 To create a development environment for RhodeCode Enterprise,
170 use the following steps:
171
172 1. Create a copy of vcsserver config:
173 `cp ~/rhodecode-vcsserver/configs/development.ini ~/rhodecode-vcsserver/configs/dev.ini`
174 2. Create a copy of rhodocode config:
175 `cp ~/rhodecode-enterprise-ce/configs/development.ini ~/rhodecode-enterprise-ce/configs/dev.ini`
176 3. Adjust the configuration settings to your needs if needed.
177
178 .. note::
179
180 It is recommended to use the name `dev.ini` since it's included in .hgignore file.
181
182
183 Setup the Development Database
184 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
185
186 To create a development database, use the following example. This is a one
187 time operation executed from the nix-shell of rhodecode-enterprise-ce sources ::
188
189 rc-setup-app dev.ini \
190 --user=admin --password=secret \
191 --email=admin@example.com \
192 --repos=~/my_dev_repos
193
194
195 Compile CSS and JavaScript
196 ^^^^^^^^^^^^^^^^^^^^^^^^^^
197
198 To use the application's frontend and prepare it for production deployment,
199 you will need to compile the CSS and JavaScript with Grunt.
200 This is easily done from within the nix-shell using the following command::
201
202 make web-build
203
204 When developing new features you will need to recompile following any
205 changes made to the CSS or JavaScript files when developing the code::
206
207 grunt watch
208
209 This prepares the development (with comments/whitespace) versions of files.
210
211 Start the Development Servers
212 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
213
214 From the rhodecode-vcsserver directory, start the development server in another
215 nix-shell, using the following command::
216
217 pserve configs/dev.ini
218
219 In the adjacent nix-shell which you created for your development server, you may
220 now start CE with the following command::
221
222
223 pserve --reload configs/dev.ini
224
225 .. note::
226
227 `--reload` flag will automatically reload the server when source file changes.
228
229
230 Run the Environment Tests
231 ^^^^^^^^^^^^^^^^^^^^^^^^^
232
233 Please make sure that the tests are passing to verify that your environment is
234 set up correctly. RhodeCode uses py.test to run tests.
235 While your instance is running, start a new nix-shell and simply run
236 ``make test`` to run the basic test suite.
237
7 Please refer to RCstack installed documentation for instructions on setting up dev environment:
8 https://docs.rhodecode.com/rcstack/dev/dev-setup.html
238 9
239 10 Need Help?
240 11 ^^^^^^^^^^
241 12
242 13 Join us on Slack via https://rhodecode.com/join or post questions in our
243 14 Community Portal at https://community.rhodecode.com
@@ -1,92 +1,104 b''
1 1 |RCE|
2 2 =====
3 3
4 4 |RCE| is a high-performance source code management and collaboration system.
5 5 It enables you to develop projects securely behind the firewall while
6 6 providing collaboration tools that work with |git|, |hg|,
7 7 and |svn| |repos|. The user interface allows you to create, edit,
8 8 and commit files and |repos| while managing their security permissions.
9 9
10 10 |RCE| provides the following features:
11 11
12 12 * Source code management.
13 13 * Extended permissions management.
14 14 * Integrated code collaboration tools.
15 15 * Integrated code review and notifications.
16 16 * Scalability provided by multi-node setup.
17 17 * Fully programmable automation API.
18 18 * Web-based hook management.
19 19 * Native |svn| support.
20 20 * Migration from existing databases.
21 21 * |RCE| SDK.
22 22 * Built-in analytics
23 23 * Built in integrations including: Slack, Webhooks (used for Jenkins/TeamCity and other CIs), Jira, Redmine, Hipchat
24 24 * Pluggable authentication system.
25 25 * Support for AD, |LDAP|, Crowd, CAS, PAM.
26 26 * Support for external authentication via Oauth Google, Github, Bitbucket, Twitter.
27 27 * Debug modes of operation.
28 28 * Private and public gists.
29 29 * Gists with limited lifetimes and within instance only sharing.
30 30 * Fully integrated code search function.
31 31 * Always on SSL connectivity.
32 32
33 33 .. only:: html
34 34
35 35 Table of Contents
36 36 -----------------
37 37
38 38 .. toctree::
39 39 :maxdepth: 1
40 :caption: Documentation directory
41
42 Back to documentation directory <https://docs.rhodecode.com/>
43
44 .. toctree::
45 :maxdepth: 1
46 :caption: RhodeCode RCstack Documentation
47
48 RhodeCode RCstack Installer <https://docs.rhodecode.com/rcstack/>
49
50 .. toctree::
51 :maxdepth: 1
40 52 :caption: Admin Documentation
41 53
42 54 install/quick-start
43 55 install/install-database
44 56 install/install-steps
45 57 admin/system-overview
46 58 admin/system-admin
47 59 admin/user-admin
48 60 admin/repo-admin
49 61 admin/security-tips
50 62 auth/auth
51 63 issue-trackers/issue-trackers
52 64 admin/lab-settings
53 65
54 66 .. toctree::
55 67 :maxdepth: 1
56 68 :caption: Feature Documentation
57 69
58 70 collaboration/collaboration
59 71 collaboration/review-notifications
60 72 collaboration/pull-requests
61 73 code-review/code-review
62 74 integrations/integrations
63 75
64 76 .. toctree::
65 77 :maxdepth: 1
66 78 :caption: User Documentation
67 79
68 80 usage/basic-usage
69 81 tutorials/tutorials
70 82
71 83 .. toctree::
72 84 :maxdepth: 1
73 85 :caption: Developer Documentation
74 86
75 87 api/api
76 88 tools/rhodecode-tools
77 89 extensions/extensions-hooks
78 90 contributing/contributing
79 91
80 92 .. toctree::
81 93 :maxdepth: 2
82 94 :caption: RhodeCode rcstack Documentation
83 95
84 96 RhodeCode Installer <https://docs.rhodecode.com/rcstack/>
85 97
86 98 .. toctree::
87 99 :maxdepth: 1
88 100 :caption: About
89 101
90 102 release-notes/release-notes
91 103 known-issues/known-issues
92 104 admin/glossary
@@ -1,92 +1,93 b''
1 1 .. _quick-start:
2 2
3 3 Quick Start Installation Guide
4 4 ==============================
5 5
6 6 .. important::
7 7
8 8 These are quick start instructions. To optimize your |RCE|,
9 9 |RCC|, and |RCT| usage, read the more detailed instructions in our guides.
10 10 For detailed installation instructions, see
11 11 :ref:`RhodeCode rcstack Documentation <rcstack:installation>`
12 12
13 13
14 14
15 15 To get |RCE| up and running, run through the below steps:
16 16
17 17 1. Register to get the latest |RCC| installer instruction from `rhodecode.com/download`_.
18 18 If you don't have an account, sign up at `rhodecode.com/register`_.
19 19
20 20 2. Run the |RCS| installer and start init process.
21 21 following example:
22 22
23 23 .. code-block:: bash
24 24
25 25 mkdir docker-rhodecode && cd docker-rhodecode
26 26 curl -L -s -o rcstack https://dls.rhodecode.com/get-rcstack && chmod +x rcstack
27 27
28 28 ./rcstack init
29 29
30 30
31 31 .. important::
32 32
33 33 We recommend running RhodeCode as a non-root user, such as `rhodecode`;
34 34 this user must have a proper home directory and sudo permissions (to start Docker)
35 35 Either log in as that user to install the software, or do it as root
36 36 with `sudo -i -u rhodecode ./rcstack init`
37 37
38 38
39 39 3. Follow instructions on |RCS| documentation pages
40 40
41 41 :ref:`Quick install tutorial <rcstack:quick_installation>`
42 42
43 43 4. Check stack status
44 44
45 45 .. code-block:: bash
46 46
47 47 ./rcstack status
48 48
49 49
50 50 Output should look similar to this:
51 51
52 52 .. code-block:: bash
53 53
54 54 ---
55 55 CONTAINER ID IMAGE STATUS NAMES PORTS
56 56 ef54fc528e3a traefik:v2.9.5 Up 2 hours rc_cluster_router-traefik-1 0.0.0.0:80->80/tcp, :::80->80/tcp
57 57 f3ea0539e8b0 rhodecode/rhodecode-ee:4.28.0 Up 2 hours (healthy) rc_cluster_apps-rhodecode-1 0.0.0.0:10020->10020/tcp, :::10020->10020/tcp
58 58 2be52ba58ffe rhodecode/rhodecode-ee:4.28.0 Up 2 hours (healthy) rc_cluster_apps-vcsserver-1
59 59 7cd730ad3263 rhodecode/rhodecode-ee:4.28.0 Up 2 hours (healthy) rc_cluster_apps-celery-1
60 60 dfa231342c87 rhodecode/rhodecode-ee:4.28.0 Up 2 hours (healthy) rc_cluster_apps-celery-beat-1
61 61 d3d76ce2de96 rhodecode/rhodecode-ee:4.28.0 Up 2 hours (healthy) rc_cluster_apps-sshd-1
62 62 daaac329414b rhodecode/rhodecode-ee:4.28.0 Up 2 hours (healthy) rc_cluster_apps-svn-1
63 63 7b8504fb9acb nginx:1.23.2 Up 2 hours (healthy) rc_cluster_services-nginx-1 80/tcp
64 64 7279c25feb6b elasticsearch:6.8.23 Up 2 hours (healthy) rc_cluster_services-elasticsearch-1 9200/tcp, 9300/tcp
65 65 19fb93587493 redis:7.0.5 Up 2 hours (healthy) rc_cluster_services-redis-1 6379/tcp
66 66 fb77fb6496c6 channelstream/channelstream:0.7.1 Up 2 hours (healthy) rc_cluster_services-channelstream-1 8000/tcp
67 67 cb6c5c022f5b postgres:14.6 Up 2 hours (healthy) rc_cluster_services-database-1 5432/tcp
68 68
69
69 70 At this point you should be able to access:
70 71
71 72 - RhodeCode instance at your domain entered, e.g http://rhodecode.local, the default access
72 73 credentials are generated and stored inside .runtime.env.
73 74 For example::
74 75
75 76 RHODECODE_USER_NAME=admin
76 77 RHODECODE_USER_PASS=super-secret-password
77 78
78 79
80
79 81 .. note::
80 82
81 83 Recommended post quick start install instructions:
82 84
83 85 * Read the documentation
84 86 * Carry out the :ref:`rhodecode-post-install-ref`
85 87 * Set up :ref:`indexing-ref`
86 88 * Familiarise yourself with the :ref:`rhodecode-admin-ref` section.
87 89
88 .. _rhodecode.com/download/: https://rhodecode.com/download/
89 90 .. _rhodecode.com: https://rhodecode.com/
90 91 .. _rhodecode.com/register: https://rhodecode.com/register/
91 92 .. _rhodecode.com/download: https://rhodecode.com/download/
92 93
@@ -1,32 +1,21 b''
1 1 .. _install-sqlite-database:
2 2
3 SQLite
4 ------
3 SQLite (Deprecated)
4 -------------------
5 5
6 6 .. important::
7 7
8 We do not recommend using SQLite in a large development environment
9 as it has an internal locking mechanism which can become a performance
10 bottleneck when there are more than 5 concurrent users.
8 As of 5.x, SQLite is no longer supported, we advise to migrate to MySQL or PostgreSQL.
11 9
12 |RCE| installs SQLite as the default database if you do not specify another
13 during installation. SQLite is suitable for small teams,
14 projects with a low load, and evaluation purposes since it is built into
15 |RCE| and does not require any additional database server.
16
17 Using MySQL or PostgreSQL in an large setup gives you much greater
18 performance, and while migration tools exist to move from one database type
19 to another, it is better to get it right first time and to immediately use
20 MySQL or PostgreSQL when you deploy |RCE| in a production environment.
21 10
22 11 Migrating From SQLite to PostgreSQL
23 12 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
24 13
25 14 If you started working with SQLite and now need to migrate your database
26 15 to PostgreSQL, you can contact support@rhodecode.com for some help. We have a
27 16 set of scripts that enable SQLite to PostgreSQL migration. These scripts have
28 17 been tested, and work with PostgreSQL 9.1+.
29 18
30 19 .. note::
31 20
32 21 There are no SQLite to MySQL or MariaDB scripts available.
@@ -1,95 +1,117 b''
1 1 .. _known-issues:
2 2
3 3 Known Issues
4 4 ============
5 5
6 6 Windows Upload
7 7 --------------
8 8
9 9 There can be an issue with uploading files from web interface on Windows,
10 10 and afterwards users cannot properly clone or synchronize with the repository.
11 11
12 12 Early testing shows that often uploading files via HTML forms on Windows
13 13 includes the full path of the file being uploaded and not the name of the file.
14 14
15 15 Old Format of Git Repositories
16 16 ------------------------------
17 17
18 18 There is an issue when trying to import old |git| format |repos| into recent
19 19 versions of |RCE|. This issue can occur when importing from external |git|
20 20 repositories or from older versions of |RCE| (<=2.2.7).
21 21
22 22 To convert the old version into a current version, clone the old
23 23 |repo| into a local machine using a recent |git| client, then push it to a new
24 24 |repo| inside |RCE|.
25 25
26 26
27 27 VCS Server Memory Consumption
28 28 -----------------------------
29 29
30 30 The VCS Server cache grows without limits if not configured correctly. This
31 31 applies to |RCE| versions prior to the 3.3.2 releases, as 3.3.2
32 32 shipped with the optimal configuration as default. See the
33 33 :ref:`vcs-server-maintain` section for details.
34 34
35 35 To fix this issue, upgrade to |RCE| 3.3.2 or greater, and if you discover
36 36 memory consumption issues check the VCS Server settings.
37 37
38 38 Newer Operating system locales
39 39 ------------------------------
40 40
41 41 |RCC| has a know problem with locales, due to changes in glibc 2.27+ which affects
42 42 the local-archive format, which is now incompatible with our used glibc 2.26.
43 43
44 44 Mostly affected are:
45
45 46 - Fedora 23+
46 47 - Ubuntu 18.04
47 48 - CentOS / RHEL 8
48 49
49 50 To work around this problem, you need set path to ``$LOCAL_ARCHIVE`` to the
50 51 locale package in older pre glibc 2.27 format, or set `LC_ALL=C` in your enviroment.
51 52
52 53 To use the pre 2.27 locale-archive fix follow these steps:
53 54
54 55 1. Download the pre 2.27 locale-archive package
55 56
56 57 .. code-block:: bash
57 58
58 59 wget https://dls.rhodecode.com/assets/locale-archive
59 60
60 61
61 62 2. Point ``$LOCAL_ARCHIVE`` to the locale package.
62 63
63 64 .. code-block:: bash
64 65
65 66 $ export LOCALE_ARCHIVE=/home/USER/locale-archive # change to your path
66 67
67 68 This should be added *both* in `enviroment` variable of `~/.rccontrol/supervisor/supervisord.ini`
68 69 e.g
69 70
70 71 ```
71 72 [supervisord]
72 73 environment = HOME=/home/user/rhodecode,LOCALE_ARCHIVE=/YOUR-PATH/locale-archive`
73 74 ```
74 75
75 76 and in user .bashrc/.zshrc etc, or via a startup script that
76 77 runs `rccontrol self-init`
77 78
78 79 If you happen to be running |RCC| from systemd, use the following
79 80 example to pass the correct locale information on boot.
80 81
81 82 .. code-block:: ini
82 83
83 84 [Unit]
84 85 Description=Rhodecode
85 86 After=network.target
86 87
87 88 [Service]
88 89 Type=forking
89 90 User=scm
90 91 Environment="LOCALE_ARCHIVE=/YOUR-PATH/locale-archive"
91 92 ExecStart=/YOUR-PATH/.rccontrol-profile/bin/rccontrol-self-init
92 93
93 94 [Install]
94 95 WantedBy=multi-user.target
95 96
97
98 Merge stucks in "merging" status
99 --------------------------------
100
101 Similar issues:
102
103 - Pull Request duplicated and/or stucks in "creating" status.
104
105 Mostly affected are:
106
107 - Kubernetes AWS EKS setup with NFS as shared storage
108 - AWS EFS as shared storage
109
110 Workaround:
111
112 1. Manually clear the repo cache via UI:
113 :menuselection:`Repository Settings --> Caches --> Invalidate repository cache`
114
115 1. Open problematic PR and reset status to "created"
116
117 Now you can merge PR normally
@@ -1,59 +1,58 b''
1 1 |RCE| 5.1.0 |RNS|
2 2 -----------------
3 3
4 4 Release Date
5 5 ^^^^^^^^^^^^
6 6
7 7 - 2024-07-18
8 8
9 9
10 10 New Features
11 11 ^^^^^^^^^^^^
12 12
13 - We've introduced 2FA for users. Now alongside the external auth 2fa support RhodeCode allows to enable 2FA for users
13 - We've introduced 2FA for users. Now alongside the external auth 2FA support RhodeCode allows to enable 2FA for users.
14 14 2FA options will be available for each user individually, or enforced via authentication plugins like ldap, or internal.
15 15 - Email based log-in. RhodeCode now allows to log-in using email as well as username for main authentication type.
16 16 - Ability to replace a file using web UI. Now one can replace an existing file from the web-ui.
17 17 - GIT LFS Sync automation. Remote push/pull commands now can also sync GIT LFS objects.
18 - Added ability to remove or close branches from the web ui
19 - Added ability to delete a branch automatically after merging PR for git repositories
20 - Added support for S3 based archive_cache based that allows storing cached archives in S3 compatible object store.
18 - Added ability to remove or close branches from the web ui.
19 - Added ability to delete a branch automatically after merging PR for git repositories.
20 - Added support for S3 based archive_cache that allows storing cached archives in S3 compatible object store.
21 21
22 22
23 23 General
24 24 ^^^^^^^
25 25
26 - Upgraded all dependency libraries to their latest available versions
26 - Upgraded all dependency libraries to their latest available versions.
27 27 - Repository storage is no longer controlled via DB settings, but .ini file. This allows easier automated deployments.
28 28 - Bumped mercurial to 6.7.4
29 29 - Mercurial: enable httppostarguments for better support of large repositories with lots of heads.
30 30 - Added explicit db-migrate step to update hooks for 5.X release.
31 31
32 32
33 33 Security
34 34 ^^^^^^^^
35 35
36 36
37 37
38 38 Performance
39 39 ^^^^^^^^^^^
40 40
41 41 - Introduced a full rewrite of ssh backend for performance. The result is 2-5x speed improvement for operation with ssh.
42 enable new ssh wrapper by setting: `ssh.wrapper_cmd = /home/rhodecode/venv/bin/rc-ssh-wrapper-v2`
43 - Introduced a new hooks subsystem that is more scalable and faster, enable it by settings: `vcs.hooks.protocol = celery`
42 Enable new ssh wrapper by setting: `ssh.wrapper_cmd = /home/rhodecode/venv/bin/rc-ssh-wrapper-v2`
43 - Introduced a new hooks subsystem that is more scalable and faster, enable it by setting: `vcs.hooks.protocol = celery`
44 44
45 45
46 46 Fixes
47 47 ^^^^^
48 48
49 - Archives: Zip archive download breaks when a gitmodules file is present
50 - Branch permissions: fixed bug preventing to specify own rules from 4.X install
51 - SVN: refactored svn events, thus fixing support for it in dockerized env
52 - Fixed empty server url in PR link after push from cli
49 - Archives: Zip archive download breaks when a gitmodules file is present.
50 - Branch permissions: fixed bug preventing to specify own rules from 4.X install.
51 - SVN: refactored svn events, thus fixing support for it in dockerized environment.
52 - Fixed empty server url in PR link after push from cli.
53 53
54 54
55 55 Upgrade notes
56 56 ^^^^^^^^^^^^^
57 57
58 - RhodeCode 5.1.0 is a mayor feature release after big 5.0.0 python3 migration. Happy to ship a first time feature
59 rich release
58 - RhodeCode 5.1.0 is a major feature release after big 5.0.0 python3 migration. Happy to ship a first time feature-rich release.
@@ -1,173 +1,175 b''
1 1 .. _rhodecode-release-notes-ref:
2 2
3 3 Release Notes
4 4 =============
5 5
6 6 |RCE| 5.x Versions
7 7 ------------------
8 8
9 9 .. toctree::
10 10 :maxdepth: 1
11 11
12
12 release-notes-5.2.0.rst
13 release-notes-5.1.2.rst
14 release-notes-5.1.1.rst
13 15 release-notes-5.1.0.rst
14 16 release-notes-5.0.3.rst
15 17 release-notes-5.0.2.rst
16 18 release-notes-5.0.1.rst
17 19 release-notes-5.0.0.rst
18 20
19 21
20 22 |RCE| 4.x Versions
21 23 ------------------
22 24
23 25 .. toctree::
24 26 :maxdepth: 1
25 27
26 28 release-notes-4.27.1.rst
27 29 release-notes-4.27.0.rst
28 30 release-notes-4.26.0.rst
29 31 release-notes-4.25.2.rst
30 32 release-notes-4.25.1.rst
31 33 release-notes-4.25.0.rst
32 34 release-notes-4.24.1.rst
33 35 release-notes-4.24.0.rst
34 36 release-notes-4.23.2.rst
35 37 release-notes-4.23.1.rst
36 38 release-notes-4.23.0.rst
37 39 release-notes-4.22.0.rst
38 40 release-notes-4.21.0.rst
39 41 release-notes-4.20.1.rst
40 42 release-notes-4.20.0.rst
41 43 release-notes-4.19.3.rst
42 44 release-notes-4.19.2.rst
43 45 release-notes-4.19.1.rst
44 46 release-notes-4.19.0.rst
45 47 release-notes-4.18.3.rst
46 48 release-notes-4.18.2.rst
47 49 release-notes-4.18.1.rst
48 50 release-notes-4.18.0.rst
49 51 release-notes-4.17.4.rst
50 52 release-notes-4.17.3.rst
51 53 release-notes-4.17.2.rst
52 54 release-notes-4.17.1.rst
53 55 release-notes-4.17.0.rst
54 56 release-notes-4.16.2.rst
55 57 release-notes-4.16.1.rst
56 58 release-notes-4.16.0.rst
57 59 release-notes-4.15.2.rst
58 60 release-notes-4.15.1.rst
59 61 release-notes-4.15.0.rst
60 62 release-notes-4.14.1.rst
61 63 release-notes-4.14.0.rst
62 64 release-notes-4.13.3.rst
63 65 release-notes-4.13.2.rst
64 66 release-notes-4.13.1.rst
65 67 release-notes-4.13.0.rst
66 68 release-notes-4.12.4.rst
67 69 release-notes-4.12.3.rst
68 70 release-notes-4.12.2.rst
69 71 release-notes-4.12.1.rst
70 72 release-notes-4.12.0.rst
71 73 release-notes-4.11.6.rst
72 74 release-notes-4.11.5.rst
73 75 release-notes-4.11.4.rst
74 76 release-notes-4.11.3.rst
75 77 release-notes-4.11.2.rst
76 78 release-notes-4.11.1.rst
77 79 release-notes-4.11.0.rst
78 80 release-notes-4.10.6.rst
79 81 release-notes-4.10.5.rst
80 82 release-notes-4.10.4.rst
81 83 release-notes-4.10.3.rst
82 84 release-notes-4.10.2.rst
83 85 release-notes-4.10.1.rst
84 86 release-notes-4.10.0.rst
85 87 release-notes-4.9.1.rst
86 88 release-notes-4.9.0.rst
87 89 release-notes-4.8.0.rst
88 90 release-notes-4.7.2.rst
89 91 release-notes-4.7.1.rst
90 92 release-notes-4.7.0.rst
91 93 release-notes-4.6.1.rst
92 94 release-notes-4.6.0.rst
93 95 release-notes-4.5.2.rst
94 96 release-notes-4.5.1.rst
95 97 release-notes-4.5.0.rst
96 98 release-notes-4.4.2.rst
97 99 release-notes-4.4.1.rst
98 100 release-notes-4.4.0.rst
99 101 release-notes-4.3.1.rst
100 102 release-notes-4.3.0.rst
101 103 release-notes-4.2.1.rst
102 104 release-notes-4.2.0.rst
103 105 release-notes-4.1.2.rst
104 106 release-notes-4.1.1.rst
105 107 release-notes-4.1.0.rst
106 108 release-notes-4.0.1.rst
107 109 release-notes-4.0.0.rst
108 110
109 111 |RCE| 3.x Versions
110 112 ------------------
111 113
112 114 .. toctree::
113 115 :maxdepth: 1
114 116
115 117 release-notes-3.8.4.rst
116 118 release-notes-3.8.3.rst
117 119 release-notes-3.8.2.rst
118 120 release-notes-3.8.1.rst
119 121 release-notes-3.8.0.rst
120 122 release-notes-3.7.1.rst
121 123 release-notes-3.7.0.rst
122 124 release-notes-3.6.1.rst
123 125 release-notes-3.6.0.rst
124 126 release-notes-3.5.2.rst
125 127 release-notes-3.5.1.rst
126 128 release-notes-3.5.0.rst
127 129 release-notes-3.4.1.rst
128 130 release-notes-3.4.0.rst
129 131 release-notes-3.3.4.rst
130 132 release-notes-3.3.3.rst
131 133 release-notes-3.3.2.rst
132 134 release-notes-3.3.1.rst
133 135 release-notes-3.3.0.rst
134 136 release-notes-3.2.3.rst
135 137 release-notes-3.2.2.rst
136 138 release-notes-3.2.1.rst
137 139 release-notes-3.2.0.rst
138 140 release-notes-3.1.1.rst
139 141 release-notes-3.1.0.rst
140 142 release-notes-3.0.2.rst
141 143 release-notes-3.0.1.rst
142 144 release-notes-3.0.0.rst
143 145
144 146 |RCE| 2.x Versions
145 147 ------------------
146 148
147 149 .. toctree::
148 150 :maxdepth: 1
149 151
150 152 release-notes-2.2.8.rst
151 153 release-notes-2.2.7.rst
152 154 release-notes-2.2.6.rst
153 155 release-notes-2.2.5.rst
154 156 release-notes-2.2.4.rst
155 157 release-notes-2.2.3.rst
156 158 release-notes-2.2.2.rst
157 159 release-notes-2.2.1.rst
158 160 release-notes-2.2.0.rst
159 161 release-notes-2.1.0.rst
160 162 release-notes-2.0.2.rst
161 163 release-notes-2.0.1.rst
162 164 release-notes-2.0.0.rst
163 165
164 166 |RCE| 1.x Versions
165 167 ------------------
166 168
167 169 .. toctree::
168 170 :maxdepth: 1
169 171
170 172 release-notes-1.7.2.rst
171 173 release-notes-1.7.1.rst
172 174 release-notes-1.7.0.rst
173 175 release-notes-1.6.0.rst
@@ -1,11 +1,11 b''
1 1 sphinx==7.2.6
2 2
3 3 furo==2023.9.10
4 4 sphinx-press-theme==0.8.0
5 5 sphinx-rtd-theme==1.3.0
6 6
7 pygments==2.16.1
7 pygments==2.18.0
8 8
9 9 docutils<0.19
10 10 markupsafe==2.1.3
11 11 jinja2==3.1.2
@@ -1,313 +1,299 b''
1 1 # deps, generated via pipdeptree --exclude setuptools,wheel,pipdeptree,pip -f | tr '[:upper:]' '[:lower:]'
2 2
3 3 alembic==1.13.1
4 4 mako==1.2.4
5 5 markupsafe==2.1.2
6 6 sqlalchemy==1.4.52
7 7 greenlet==3.0.3
8 typing_extensions==4.9.0
8 typing_extensions==4.12.2
9 9 async-timeout==4.0.3
10 10 babel==2.12.1
11 11 beaker==1.12.1
12 12 celery==5.3.6
13 13 billiard==4.2.0
14 14 click==8.1.3
15 15 click-didyoumean==0.3.0
16 16 click==8.1.3
17 17 click-plugins==1.1.1
18 18 click==8.1.3
19 19 click-repl==0.2.0
20 20 click==8.1.3
21 prompt-toolkit==3.0.38
22 wcwidth==0.2.6
21 prompt_toolkit==3.0.47
22 wcwidth==0.2.13
23 23 six==1.16.0
24 24 kombu==5.3.5
25 25 amqp==5.2.0
26 26 vine==5.1.0
27 27 vine==5.1.0
28 28 python-dateutil==2.8.2
29 29 six==1.16.0
30 30 tzdata==2024.1
31 31 vine==5.1.0
32 32 channelstream==0.7.1
33 33 gevent==24.2.1
34 34 greenlet==3.0.3
35 35 zope.event==5.0.0
36 zope.interface==6.3.0
36 zope.interface==7.0.3
37 37 itsdangerous==1.1.0
38 38 marshmallow==2.18.0
39 39 pyramid==2.0.2
40 40 hupper==1.12
41 41 plaster==1.1.2
42 42 plaster-pastedeploy==1.0.1
43 43 pastedeploy==3.1.0
44 44 plaster==1.1.2
45 45 translationstring==1.4
46 46 venusian==3.0.0
47 47 webob==1.8.7
48 48 zope.deprecation==5.0.0
49 zope.interface==6.3.0
49 zope.interface==7.0.3
50 50 pyramid-jinja2==2.10
51 51 jinja2==3.1.2
52 52 markupsafe==2.1.2
53 53 markupsafe==2.1.2
54 54 pyramid==2.0.2
55 55 hupper==1.12
56 56 plaster==1.1.2
57 57 plaster-pastedeploy==1.0.1
58 58 pastedeploy==3.1.0
59 59 plaster==1.1.2
60 60 translationstring==1.4
61 61 venusian==3.0.0
62 62 webob==1.8.7
63 63 zope.deprecation==5.0.0
64 zope.interface==6.3.0
64 zope.interface==7.0.3
65 65 zope.deprecation==5.0.0
66 66 python-dateutil==2.8.2
67 67 six==1.16.0
68 68 requests==2.28.2
69 69 certifi==2022.12.7
70 70 charset-normalizer==3.1.0
71 71 idna==3.4
72 72 urllib3==1.26.14
73 73 ws4py==0.5.1
74 74 deform==2.0.15
75 75 chameleon==3.10.2
76 76 colander==2.0
77 77 iso8601==1.1.0
78 78 translationstring==1.4
79 79 iso8601==1.1.0
80 80 peppercorn==0.6
81 81 translationstring==1.4
82 82 zope.deprecation==5.0.0
83 83 docutils==0.19
84 84 dogpile.cache==1.3.3
85 85 decorator==5.1.1
86 86 stevedore==5.1.0
87 87 pbr==5.11.1
88 88 formencode==2.1.0
89 89 six==1.16.0
90 fsspec==2024.6.0
91 gunicorn==21.2.0
92 packaging==24.0
90 fsspec==2024.9.0
91 gunicorn==23.0.0
92 packaging==24.1
93 93 gevent==24.2.1
94 94 greenlet==3.0.3
95 95 zope.event==5.0.0
96 zope.interface==6.3.0
97 ipython==8.14.0
98 backcall==0.2.0
96 zope.interface==7.0.3
97 ipython==8.26.0
99 98 decorator==5.1.1
100 jedi==0.19.0
101 parso==0.8.3
102 matplotlib-inline==0.1.6
103 traitlets==5.9.0
104 pexpect==4.8.0
99 jedi==0.19.1
100 parso==0.8.4
101 matplotlib-inline==0.1.7
102 traitlets==5.14.3
103 pexpect==4.9.0
105 104 ptyprocess==0.7.0
106 pickleshare==0.7.5
107 prompt-toolkit==3.0.38
108 wcwidth==0.2.6
109 pygments==2.15.1
110 stack-data==0.6.2
111 asttokens==2.2.1
105 prompt_toolkit==3.0.47
106 wcwidth==0.2.13
107 pygments==2.18.0
108 stack-data==0.6.3
109 asttokens==2.4.1
112 110 six==1.16.0
113 executing==1.2.0
114 pure-eval==0.2.2
115 traitlets==5.9.0
111 executing==2.0.1
112 pure_eval==0.2.3
113 traitlets==5.14.3
114 typing_extensions==4.12.2
116 115 markdown==3.4.3
117 116 msgpack==1.0.8
118 117 mysqlclient==2.1.1
119 118 nbconvert==7.7.3
120 119 beautifulsoup4==4.12.3
121 120 soupsieve==2.5
122 121 bleach==6.1.0
123 122 six==1.16.0
124 123 webencodings==0.5.1
125 124 defusedxml==0.7.1
126 125 jinja2==3.1.2
127 126 markupsafe==2.1.2
128 127 jupyter_core==5.3.1
129 128 platformdirs==3.10.0
130 traitlets==5.9.0
129 traitlets==5.14.3
131 130 jupyterlab-pygments==0.2.2
132 131 markupsafe==2.1.2
133 132 mistune==2.0.5
134 133 nbclient==0.8.0
135 134 jupyter_client==8.3.0
136 135 jupyter_core==5.3.1
137 136 platformdirs==3.10.0
138 traitlets==5.9.0
137 traitlets==5.14.3
139 138 python-dateutil==2.8.2
140 139 six==1.16.0
141 140 pyzmq==25.0.0
142 141 tornado==6.2
143 traitlets==5.9.0
142 traitlets==5.14.3
144 143 jupyter_core==5.3.1
145 144 platformdirs==3.10.0
146 traitlets==5.9.0
145 traitlets==5.14.3
147 146 nbformat==5.9.2
148 147 fastjsonschema==2.18.0
149 148 jsonschema==4.18.6
150 149 attrs==22.2.0
151 150 pyrsistent==0.19.3
152 151 jupyter_core==5.3.1
153 152 platformdirs==3.10.0
154 traitlets==5.9.0
155 traitlets==5.9.0
156 traitlets==5.9.0
153 traitlets==5.14.3
154 traitlets==5.14.3
155 traitlets==5.14.3
157 156 nbformat==5.9.2
158 157 fastjsonschema==2.18.0
159 158 jsonschema==4.18.6
160 159 attrs==22.2.0
161 160 pyrsistent==0.19.3
162 161 jupyter_core==5.3.1
163 162 platformdirs==3.10.0
164 traitlets==5.9.0
165 traitlets==5.9.0
163 traitlets==5.14.3
164 traitlets==5.14.3
166 165 pandocfilters==1.5.0
167 pygments==2.15.1
166 pygments==2.18.0
168 167 tinycss2==1.2.1
169 168 webencodings==0.5.1
170 traitlets==5.9.0
171 orjson==3.10.3
169 traitlets==5.14.3
170 orjson==3.10.7
172 171 paste==3.10.1
173 172 premailer==3.10.0
174 173 cachetools==5.3.3
175 174 cssselect==1.2.0
176 175 cssutils==2.6.0
177 lxml==4.9.3
176 lxml==5.3.0
178 177 requests==2.28.2
179 178 certifi==2022.12.7
180 179 charset-normalizer==3.1.0
181 180 idna==3.4
182 181 urllib3==1.26.14
183 182 psutil==5.9.8
184 183 psycopg2==2.9.9
185 184 py-bcrypt==0.4
186 185 pycmarkgfm==1.2.0
187 186 cffi==1.16.0
188 187 pycparser==2.21
189 188 pycryptodome==3.17
190 189 pycurl==7.45.3
191 190 pymysql==1.0.3
192 191 pyotp==2.8.0
193 192 pyparsing==3.1.1
194 pyramid-debugtoolbar==4.12.1
195 pygments==2.15.1
196 pyramid==2.0.2
197 hupper==1.12
198 plaster==1.1.2
199 plaster-pastedeploy==1.0.1
200 pastedeploy==3.1.0
201 plaster==1.1.2
202 translationstring==1.4
203 venusian==3.0.0
204 webob==1.8.7
205 zope.deprecation==5.0.0
206 zope.interface==6.3.0
207 pyramid-mako==1.1.0
208 mako==1.2.4
209 markupsafe==2.1.2
210 pyramid==2.0.2
211 hupper==1.12
212 plaster==1.1.2
213 plaster-pastedeploy==1.0.1
214 pastedeploy==3.1.0
215 plaster==1.1.2
216 translationstring==1.4
217 venusian==3.0.0
218 webob==1.8.7
219 zope.deprecation==5.0.0
220 zope.interface==6.3.0
221 193 pyramid-mailer==0.15.1
222 194 pyramid==2.0.2
223 195 hupper==1.12
224 196 plaster==1.1.2
225 197 plaster-pastedeploy==1.0.1
226 198 pastedeploy==3.1.0
227 199 plaster==1.1.2
228 200 translationstring==1.4
229 201 venusian==3.0.0
230 202 webob==1.8.7
231 203 zope.deprecation==5.0.0
232 zope.interface==6.3.0
204 zope.interface==7.0.3
233 205 repoze.sendmail==4.4.1
234 transaction==3.1.0
235 zope.interface==6.3.0
236 zope.interface==6.3.0
237 transaction==3.1.0
238 zope.interface==6.3.0
206 transaction==5.0.0
207 zope.interface==7.0.3
208 zope.interface==7.0.3
209 transaction==5.0.0
210 zope.interface==7.0.3
211 pyramid-mako==1.1.0
212 mako==1.2.4
213 markupsafe==2.1.2
214 pyramid==2.0.2
215 hupper==1.12
216 plaster==1.1.2
217 plaster-pastedeploy==1.0.1
218 pastedeploy==3.1.0
219 plaster==1.1.2
220 translationstring==1.4
221 venusian==3.0.0
222 webob==1.8.7
223 zope.deprecation==5.0.0
224 zope.interface==7.0.3
239 225 python-ldap==3.4.3
240 226 pyasn1==0.4.8
241 227 pyasn1-modules==0.2.8
242 228 pyasn1==0.4.8
243 229 python-memcached==1.59
244 230 six==1.16.0
245 231 python-pam==2.0.2
246 python3-saml==1.15.0
232 python3-saml==1.16.0
247 233 isodate==0.6.1
248 234 six==1.16.0
249 lxml==4.9.3
250 xmlsec==1.3.13
251 lxml==4.9.3
235 lxml==5.3.0
236 xmlsec==1.3.14
237 lxml==5.3.0
252 238 pyyaml==6.0.1
253 redis==5.0.4
239 redis==5.1.0
254 240 async-timeout==4.0.3
255 241 regex==2022.10.31
256 242 routes==2.5.1
257 243 repoze.lru==0.7
258 244 six==1.16.0
259 s3fs==2024.6.0
245 s3fs==2024.9.0
260 246 aiobotocore==2.13.0
261 247 aiohttp==3.9.5
262 248 aiosignal==1.3.1
263 249 frozenlist==1.4.1
264 250 attrs==22.2.0
265 251 frozenlist==1.4.1
266 252 multidict==6.0.5
267 253 yarl==1.9.4
268 254 idna==3.4
269 255 multidict==6.0.5
270 256 aioitertools==0.11.0
271 257 botocore==1.34.106
272 258 jmespath==1.0.1
273 259 python-dateutil==2.8.2
274 260 six==1.16.0
275 261 urllib3==1.26.14
276 262 wrapt==1.16.0
277 263 aiohttp==3.9.5
278 264 aiosignal==1.3.1
279 265 frozenlist==1.4.1
280 266 attrs==22.2.0
281 267 frozenlist==1.4.1
282 268 multidict==6.0.5
283 269 yarl==1.9.4
284 270 idna==3.4
285 271 multidict==6.0.5
286 fsspec==2024.6.0
272 fsspec==2024.9.0
287 273 simplejson==3.19.2
288 274 sshpubkeys==3.3.1
289 275 cryptography==40.0.2
290 276 cffi==1.16.0
291 277 pycparser==2.21
292 278 ecdsa==0.18.0
293 279 six==1.16.0
294 280 sqlalchemy==1.4.52
295 281 greenlet==3.0.3
296 typing_extensions==4.9.0
282 typing_extensions==4.12.2
297 283 supervisor==4.2.5
298 284 tzlocal==4.3
299 285 pytz-deprecation-shim==0.1.0.post0
300 286 tzdata==2024.1
301 287 tempita==0.5.2
302 288 unidecode==1.3.6
303 289 urlobject==2.4.3
304 290 waitress==3.0.0
305 291 webhelpers2==2.1
306 292 markupsafe==2.1.2
307 293 six==1.16.0
308 294 whoosh==2.7.4
309 295 zope.cachedescriptors==5.0.0
310 296 qrcode==7.4.2
311 297
312 298 ## uncomment to add the debug libraries
313 299 #-r requirements_debug.txt
@@ -1,28 +1,29 b''
1 1 ## special libraries we could extend the requirements.txt file with to add some
2 2 ## custom libraries usefull for debug and memory tracing
3 3
4 4 objgraph
5 5 memory-profiler
6 6 pympler
7 7
8 8 ## debug
9 9 ipdb
10 10 ipython
11 11 rich
12 pyramid-debugtoolbar
12 13
13 14 # format
14 15 flake8
15 16 ruff
16 17
17 18 pipdeptree==2.7.1
18 19 invoke==2.0.0
19 20 bumpversion==0.6.0
20 21 bump2version==1.0.1
21 22
22 23 docutils-stubs
23 24 types-redis
24 25 types-requests==2.31.0.6
25 26 types-sqlalchemy
26 27 types-psutil
27 28 types-pycurl
28 29 types-ujson
@@ -1,48 +1,48 b''
1 1 # test related requirements
2 2 mock==5.1.0
3 3 pytest-cov==4.1.0
4 4 coverage==7.4.3
5 5 pytest==8.1.1
6 6 iniconfig==2.0.0
7 packaging==24.0
7 packaging==24.1
8 8 pluggy==1.4.0
9 9 pytest-env==1.1.3
10 10 pytest==8.1.1
11 11 iniconfig==2.0.0
12 packaging==24.0
12 packaging==24.1
13 13 pluggy==1.4.0
14 14 pytest-profiling==1.7.0
15 15 gprof2dot==2022.7.29
16 16 pytest==8.1.1
17 17 iniconfig==2.0.0
18 packaging==24.0
18 packaging==24.1
19 19 pluggy==1.4.0
20 20 six==1.16.0
21 21 pytest-rerunfailures==13.0
22 packaging==24.0
22 packaging==24.1
23 23 pytest==8.1.1
24 24 iniconfig==2.0.0
25 packaging==24.0
25 packaging==24.1
26 26 pluggy==1.4.0
27 27 pytest-runner==6.0.1
28 28 pytest-sugar==1.0.0
29 packaging==24.0
29 packaging==24.1
30 30 pytest==8.1.1
31 31 iniconfig==2.0.0
32 packaging==24.0
32 packaging==24.1
33 33 pluggy==1.4.0
34 34 termcolor==2.4.0
35 35 pytest-timeout==2.3.1
36 36 pytest==8.1.1
37 37 iniconfig==2.0.0
38 packaging==24.0
38 packaging==24.1
39 39 pluggy==1.4.0
40 40 webtest==3.0.0
41 41 beautifulsoup4==4.12.3
42 42 soupsieve==2.5
43 43 waitress==3.0.0
44 44 webob==1.8.7
45 45
46 46 # RhodeCode test-data
47 47 rc_testdata @ https://code.rhodecode.com/upstream/rc-testdata-dist/raw/77378e9097f700b4c1b9391b56199fe63566b5c9/rc_testdata-0.11.0.tar.gz#egg=rc_testdata
48 48 rc_testdata==0.11.0
@@ -1,1 +1,1 b''
1 5.1.2 No newline at end of file
1 5.2.0
@@ -1,581 +1,581 b''
1 1 # Copyright (C) 2011-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import itertools
20 20 import logging
21 21 import sys
22 22 import fnmatch
23 23
24 24 import decorator
25 25 import venusian
26 26 from collections import OrderedDict
27 27
28 28 from pyramid.exceptions import ConfigurationError
29 29 from pyramid.renderers import render
30 30 from pyramid.response import Response
31 31 from pyramid.httpexceptions import HTTPNotFound
32 32
33 33 from rhodecode.api.exc import (
34 34 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
35 35 from rhodecode.apps._base import TemplateArgs
36 36 from rhodecode.lib.auth import AuthUser
37 37 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
38 38 from rhodecode.lib.exc_tracking import store_exception
39 39 from rhodecode.lib import ext_json
40 40 from rhodecode.lib.utils2 import safe_str
41 41 from rhodecode.lib.plugins.utils import get_plugin_settings
42 42 from rhodecode.model.db import User, UserApiKeys
43 from rhodecode.config.patches import inspect_getargspec
43 44
44 45 log = logging.getLogger(__name__)
45 46
46 47 DEFAULT_RENDERER = 'jsonrpc_renderer'
47 48 DEFAULT_URL = '/_admin/api'
48 49 SERVICE_API_IDENTIFIER = 'service_'
49 50
50 51
51 52 def find_methods(jsonrpc_methods, pattern):
52 53 matches = OrderedDict()
53 54 if not isinstance(pattern, (list, tuple)):
54 55 pattern = [pattern]
55 56
56 57 for single_pattern in pattern:
57 58 for method_name, method in filter(
58 59 lambda x: not x[0].startswith(SERVICE_API_IDENTIFIER), jsonrpc_methods.items()
59 60 ):
60 61 if fnmatch.fnmatch(method_name, single_pattern):
61 62 matches[method_name] = method
62 63 return matches
63 64
64 65
65 66 class ExtJsonRenderer(object):
66 67 """
67 68 Custom renderer that makes use of our ext_json lib
68 69
69 70 """
70 71
71 72 def __init__(self):
72 73 self.serializer = ext_json.formatted_json
73 74
74 75 def __call__(self, info):
75 76 """ Returns a plain JSON-encoded string with content-type
76 77 ``application/json``. The content-type may be overridden by
77 78 setting ``request.response.content_type``."""
78 79
79 80 def _render(value, system):
80 81 request = system.get('request')
81 82 if request is not None:
82 83 response = request.response
83 84 ct = response.content_type
84 85 if ct == response.default_content_type:
85 86 response.content_type = 'application/json'
86 87
87 88 return self.serializer(value)
88 89
89 90 return _render
90 91
91 92
92 93 def jsonrpc_response(request, result):
93 94 rpc_id = getattr(request, 'rpc_id', None)
94 95
95 96 ret_value = ''
96 97 if rpc_id:
97 98 ret_value = {'id': rpc_id, 'result': result, 'error': None}
98 99
99 100 # fetch deprecation warnings, and store it inside results
100 101 deprecation = getattr(request, 'rpc_deprecation', None)
101 102 if deprecation:
102 103 ret_value['DEPRECATION_WARNING'] = deprecation
103 104
104 105 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
105 106 content_type = 'application/json'
106 107 content_type_header = 'Content-Type'
107 108 headers = {
108 109 content_type_header: content_type
109 110 }
110 111 return Response(
111 112 body=raw_body,
112 113 content_type=content_type,
113 114 headerlist=[(k, v) for k, v in headers.items()]
114 115 )
115 116
116 117
117 118 def jsonrpc_error(request, message, retid=None, code: int | None = None, headers: dict | None = None):
118 119 """
119 120 Generate a Response object with a JSON-RPC error body
120 121 """
121 122 headers = headers or {}
122 123 content_type = 'application/json'
123 124 content_type_header = 'Content-Type'
124 125 if content_type_header not in headers:
125 126 headers[content_type_header] = content_type
126 127
127 128 err_dict = {'id': retid, 'result': None, 'error': message}
128 129 raw_body = render(DEFAULT_RENDERER, err_dict, request=request)
129 130
130 131 return Response(
131 132 body=raw_body,
132 133 status=code,
133 134 content_type=content_type,
134 135 headerlist=[(k, v) for k, v in headers.items()]
135 136 )
136 137
137 138
138 139 def exception_view(exc, request):
139 140 rpc_id = getattr(request, 'rpc_id', None)
140 141
141 142 if isinstance(exc, JSONRPCError):
142 143 fault_message = safe_str(exc)
143 144 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
144 145 elif isinstance(exc, JSONRPCValidationError):
145 146 colander_exc = exc.colander_exception
146 147 # TODO(marcink): think maybe of nicer way to serialize errors ?
147 148 fault_message = colander_exc.asdict()
148 149 log.debug('json-rpc colander error rpc_id:%s "%s"', rpc_id, fault_message)
149 150 elif isinstance(exc, JSONRPCForbidden):
150 151 fault_message = 'Access was denied to this resource.'
151 152 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
152 153 elif isinstance(exc, HTTPNotFound):
153 154 method = request.rpc_method
154 155 log.debug('json-rpc method `%s` not found in list of '
155 156 'api calls: %s, rpc_id:%s',
156 157 method, list(request.registry.jsonrpc_methods.keys()), rpc_id)
157 158
158 159 similar = 'none'
159 160 try:
160 161 similar_paterns = [f'*{x}*' for x in method.split('_')]
161 162 similar_found = find_methods(
162 163 request.registry.jsonrpc_methods, similar_paterns)
163 164 similar = ', '.join(similar_found.keys()) or similar
164 165 except Exception:
165 166 # make the whole above block safe
166 167 pass
167 168
168 169 fault_message = f"No such method: {method}. Similar methods: {similar}"
169 170 else:
170 171 fault_message = 'undefined error'
171 172 exc_info = exc.exc_info()
172 173 store_exception(id(exc_info), exc_info, prefix='rhodecode-api')
173 174
174 175 statsd = request.registry.statsd
175 176 if statsd:
176 177 exc_type = f"{exc.__class__.__module__}.{exc.__class__.__name__}"
177 178 statsd.incr('rhodecode_exception_total',
178 179 tags=["exc_source:api", f"type:{exc_type}"])
179 180
180 181 return jsonrpc_error(request, fault_message, rpc_id)
181 182
182 183
183 184 def request_view(request):
184 185 """
185 186 Main request handling method. It handles all logic to call a specific
186 187 exposed method
187 188 """
188 189 # cython compatible inspect
189 from rhodecode.config.patches import inspect_getargspec
190 190 inspect = inspect_getargspec()
191 191
192 192 # check if we can find this session using api_key, get_by_auth_token
193 193 # search not expired tokens only
194 194 try:
195 195 if not request.rpc_method.startswith(SERVICE_API_IDENTIFIER):
196 196 api_user = User.get_by_auth_token(request.rpc_api_key)
197 197
198 198 if api_user is None:
199 199 return jsonrpc_error(
200 200 request, retid=request.rpc_id, message='Invalid API KEY')
201 201
202 202 if not api_user.active:
203 203 return jsonrpc_error(
204 204 request, retid=request.rpc_id,
205 205 message='Request from this user not allowed')
206 206
207 207 # check if we are allowed to use this IP
208 208 auth_u = AuthUser(
209 209 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
210 210 if not auth_u.ip_allowed:
211 211 return jsonrpc_error(
212 212 request, retid=request.rpc_id,
213 213 message='Request from IP:{} not allowed'.format(
214 214 request.rpc_ip_addr))
215 215 else:
216 216 log.info('Access for IP:%s allowed', request.rpc_ip_addr)
217 217
218 218 # register our auth-user
219 219 request.rpc_user = auth_u
220 220 request.environ['rc_auth_user_id'] = str(auth_u.user_id)
221 221
222 222 # now check if token is valid for API
223 223 auth_token = request.rpc_api_key
224 224 token_match = api_user.authenticate_by_token(
225 225 auth_token, roles=[UserApiKeys.ROLE_API])
226 226 invalid_token = not token_match
227 227
228 228 log.debug('Checking if API KEY is valid with proper role')
229 229 if invalid_token:
230 230 return jsonrpc_error(
231 231 request, retid=request.rpc_id,
232 232 message='API KEY invalid or, has bad role for an API call')
233 233 else:
234 234 auth_u = 'service'
235 235 if request.rpc_api_key != request.registry.settings['app.service_api.token']:
236 236 raise Exception("Provided service secret is not recognized!")
237 237
238 238 except Exception:
239 239 log.exception('Error on API AUTH')
240 240 return jsonrpc_error(
241 241 request, retid=request.rpc_id, message='Invalid API KEY')
242 242
243 243 method = request.rpc_method
244 244 func = request.registry.jsonrpc_methods[method]
245 245
246 246 # now that we have a method, add request._req_params to
247 247 # self.kargs and dispatch control to WGIController
248 248
249 249 argspec = inspect.getargspec(func)
250 250 arglist = argspec[0]
251 251 defs = argspec[3] or []
252 252 defaults = [type(a) for a in defs]
253 253 default_empty = type(NotImplemented)
254 254
255 255 # kw arguments required by this method
256 256 func_kwargs = dict(itertools.zip_longest(
257 257 reversed(arglist), reversed(defaults), fillvalue=default_empty))
258 258
259 259 # This attribute will need to be first param of a method that uses
260 260 # api_key, which is translated to instance of user at that name
261 261 user_var = 'apiuser'
262 262 request_var = 'request'
263 263
264 264 for arg in [user_var, request_var]:
265 265 if arg not in arglist:
266 266 return jsonrpc_error(
267 267 request,
268 268 retid=request.rpc_id,
269 269 message='This method [%s] does not support '
270 270 'required parameter `%s`' % (func.__name__, arg))
271 271
272 272 # get our arglist and check if we provided them as args
273 273 for arg, default in func_kwargs.items():
274 274 if arg in [user_var, request_var]:
275 275 # user_var and request_var are pre-hardcoded parameters and we
276 276 # don't need to do any translation
277 277 continue
278 278
279 279 # skip the required param check if it's default value is
280 280 # NotImplementedType (default_empty)
281 281 if default == default_empty and arg not in request.rpc_params:
282 282 return jsonrpc_error(
283 283 request,
284 284 retid=request.rpc_id,
285 285 message=('Missing non optional `%s` arg in JSON DATA' % arg)
286 286 )
287 287
288 288 # sanitize extra passed arguments
289 289 for k in list(request.rpc_params.keys()):
290 290 if k not in func_kwargs:
291 291 del request.rpc_params[k]
292 292
293 293 call_params = request.rpc_params
294 294 call_params.update({
295 295 'request': request,
296 296 'apiuser': auth_u
297 297 })
298 298
299 299 # register some common functions for usage
300 300 rpc_user = request.rpc_user.user_id if hasattr(request, 'rpc_user') else None
301 301 attach_context_attributes(TemplateArgs(), request, rpc_user)
302 302
303 303 statsd = request.registry.statsd
304 304
305 305 try:
306 306 ret_value = func(**call_params)
307 307 resp = jsonrpc_response(request, ret_value)
308 308 if statsd:
309 309 statsd.incr('rhodecode_api_call_success_total')
310 310 return resp
311 311 except JSONRPCBaseError:
312 312 raise
313 313 except Exception:
314 314 log.exception('Unhandled exception occurred on api call: %s', func)
315 315 exc_info = sys.exc_info()
316 316 exc_id, exc_type_name = store_exception(
317 317 id(exc_info), exc_info, prefix='rhodecode-api')
318 318 error_headers = {
319 319 'RhodeCode-Exception-Id': str(exc_id),
320 320 'RhodeCode-Exception-Type': str(exc_type_name)
321 321 }
322 322 err_resp = jsonrpc_error(
323 323 request, retid=request.rpc_id, message='Internal server error',
324 324 headers=error_headers)
325 325 if statsd:
326 326 statsd.incr('rhodecode_api_call_fail_total')
327 327 return err_resp
328 328
329 329
330 330 def setup_request(request):
331 331 """
332 332 Parse a JSON-RPC request body. It's used inside the predicates method
333 333 to validate and bootstrap requests for usage in rpc calls.
334 334
335 335 We need to raise JSONRPCError here if we want to return some errors back to
336 336 user.
337 337 """
338 338
339 339 log.debug('Executing setup request: %r', request)
340 340 request.rpc_ip_addr = get_ip_addr(request.environ)
341 341 # TODO(marcink): deprecate GET at some point
342 342 if request.method not in ['POST', 'GET']:
343 343 log.debug('unsupported request method "%s"', request.method)
344 344 raise JSONRPCError(
345 345 'unsupported request method "%s". Please use POST' % request.method)
346 346
347 347 if 'CONTENT_LENGTH' not in request.environ:
348 348 log.debug("No Content-Length")
349 349 raise JSONRPCError("Empty body, No Content-Length in request")
350 350
351 351 else:
352 352 length = request.environ['CONTENT_LENGTH']
353 353 log.debug('Content-Length: %s', length)
354 354
355 355 if length == 0:
356 356 log.debug("Content-Length is 0")
357 357 raise JSONRPCError("Content-Length is 0")
358 358
359 359 raw_body = request.body
360 360 log.debug("Loading JSON body now")
361 361 try:
362 362 json_body = ext_json.json.loads(raw_body)
363 363 except ValueError as e:
364 364 # catch JSON errors Here
365 365 raise JSONRPCError(f"JSON parse error ERR:{e} RAW:{raw_body!r}")
366 366
367 367 request.rpc_id = json_body.get('id')
368 368 request.rpc_method = json_body.get('method')
369 369
370 370 # check required base parameters
371 371 try:
372 372 api_key = json_body.get('api_key')
373 373 if not api_key:
374 374 api_key = json_body.get('auth_token')
375 375
376 376 if not api_key:
377 377 raise KeyError('api_key or auth_token')
378 378
379 379 # TODO(marcink): support passing in token in request header
380 380
381 381 request.rpc_api_key = api_key
382 382 request.rpc_id = json_body['id']
383 383 request.rpc_method = json_body['method']
384 384 request.rpc_params = json_body['args'] \
385 385 if isinstance(json_body['args'], dict) else {}
386 386
387 387 log.debug('method: %s, params: %.10240r', request.rpc_method, request.rpc_params)
388 388 except KeyError as e:
389 389 raise JSONRPCError(f'Incorrect JSON data. Missing {e}')
390 390
391 391 log.debug('setup complete, now handling method:%s rpcid:%s',
392 392 request.rpc_method, request.rpc_id, )
393 393
394 394
395 395 class RoutePredicate(object):
396 396 def __init__(self, val, config):
397 397 self.val = val
398 398
399 399 def text(self):
400 400 return f'jsonrpc route = {self.val}'
401 401
402 402 phash = text
403 403
404 404 def __call__(self, info, request):
405 405 if self.val:
406 406 # potentially setup and bootstrap our call
407 407 setup_request(request)
408 408
409 409 # Always return True so that even if it isn't a valid RPC it
410 410 # will fall through to the underlaying handlers like notfound_view
411 411 return True
412 412
413 413
414 414 class NotFoundPredicate(object):
415 415 def __init__(self, val, config):
416 416 self.val = val
417 417 self.methods = config.registry.jsonrpc_methods
418 418
419 419 def text(self):
420 420 return f'jsonrpc method not found = {self.val}'
421 421
422 422 phash = text
423 423
424 424 def __call__(self, info, request):
425 425 return hasattr(request, 'rpc_method')
426 426
427 427
428 428 class MethodPredicate(object):
429 429 def __init__(self, val, config):
430 430 self.method = val
431 431
432 432 def text(self):
433 433 return f'jsonrpc method = {self.method}'
434 434
435 435 phash = text
436 436
437 437 def __call__(self, context, request):
438 438 # we need to explicitly return False here, so pyramid doesn't try to
439 439 # execute our view directly. We need our main handler to execute things
440 440 return getattr(request, 'rpc_method') == self.method
441 441
442 442
443 443 def add_jsonrpc_method(config, view, **kwargs):
444 444 # pop the method name
445 445 method = kwargs.pop('method', None)
446 446
447 447 if method is None:
448 448 raise ConfigurationError(
449 449 'Cannot register a JSON-RPC method without specifying the "method"')
450 450
451 451 # we define custom predicate, to enable to detect conflicting methods,
452 452 # those predicates are kind of "translation" from the decorator variables
453 453 # to internal predicates names
454 454
455 455 kwargs['jsonrpc_method'] = method
456 456
457 457 # register our view into global view store for validation
458 458 config.registry.jsonrpc_methods[method] = view
459 459
460 460 # we're using our main request_view handler, here, so each method
461 461 # has a unified handler for itself
462 462 config.add_view(request_view, route_name='apiv2', **kwargs)
463 463
464 464
465 465 class jsonrpc_method(object):
466 466 """
467 467 decorator that works similar to @add_view_config decorator,
468 468 but tailored for our JSON RPC
469 469 """
470 470
471 471 venusian = venusian # for testing injection
472 472
473 473 def __init__(self, method=None, **kwargs):
474 474 self.method = method
475 475 self.kwargs = kwargs
476 476
477 477 def __call__(self, wrapped):
478 478 kwargs = self.kwargs.copy()
479 479 kwargs['method'] = self.method or wrapped.__name__
480 480 depth = kwargs.pop('_depth', 0)
481 481
482 482 def callback(context, name, ob):
483 483 config = context.config.with_package(info.module)
484 484 config.add_jsonrpc_method(view=ob, **kwargs)
485 485
486 486 info = venusian.attach(wrapped, callback, category='pyramid',
487 487 depth=depth + 1)
488 488 if info.scope == 'class':
489 489 # ensure that attr is set if decorating a class method
490 490 kwargs.setdefault('attr', wrapped.__name__)
491 491
492 492 kwargs['_info'] = info.codeinfo # fbo action_method
493 493 return wrapped
494 494
495 495
496 496 class jsonrpc_deprecated_method(object):
497 497 """
498 498 Marks method as deprecated, adds log.warning, and inject special key to
499 499 the request variable to mark method as deprecated.
500 500 Also injects special docstring that extract_docs will catch to mark
501 501 method as deprecated.
502 502
503 503 :param use_method: specify which method should be used instead of
504 504 the decorated one
505 505
506 506 Use like::
507 507
508 508 @jsonrpc_method()
509 509 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
510 510 def old_func(request, apiuser, arg1, arg2):
511 511 ...
512 512 """
513 513
514 514 def __init__(self, use_method, deprecated_at_version):
515 515 self.use_method = use_method
516 516 self.deprecated_at_version = deprecated_at_version
517 517 self.deprecated_msg = ''
518 518
519 519 def __call__(self, func):
520 520 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
521 521 method=self.use_method)
522 522
523 523 docstring = """\n
524 524 .. deprecated:: {version}
525 525
526 526 {deprecation_message}
527 527
528 528 {original_docstring}
529 529 """
530 530 func.__doc__ = docstring.format(
531 531 version=self.deprecated_at_version,
532 532 deprecation_message=self.deprecated_msg,
533 533 original_docstring=func.__doc__)
534 534 return decorator.decorator(self.__wrapper, func)
535 535
536 536 def __wrapper(self, func, *fargs, **fkwargs):
537 537 log.warning('DEPRECATED API CALL on function %s, please '
538 538 'use `%s` instead', func, self.use_method)
539 539 # alter function docstring to mark as deprecated, this is picked up
540 540 # via fabric file that generates API DOC.
541 541 result = func(*fargs, **fkwargs)
542 542
543 543 request = fargs[0]
544 544 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
545 545 return result
546 546
547 547
548 548 def add_api_methods(config):
549 549 from rhodecode.api.views import (
550 550 deprecated_api, gist_api, pull_request_api, repo_api, repo_group_api,
551 551 server_api, search_api, testing_api, user_api, user_group_api)
552 552
553 553 config.scan('rhodecode.api.views')
554 554
555 555
556 556 def includeme(config):
557 557 plugin_module = 'rhodecode.api'
558 558 plugin_settings = get_plugin_settings(
559 559 plugin_module, config.registry.settings)
560 560
561 561 if not hasattr(config.registry, 'jsonrpc_methods'):
562 562 config.registry.jsonrpc_methods = OrderedDict()
563 563
564 564 # match filter by given method only
565 565 config.add_view_predicate('jsonrpc_method', MethodPredicate)
566 566 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
567 567
568 568 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer())
569 569 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
570 570
571 571 config.add_route_predicate(
572 572 'jsonrpc_call', RoutePredicate)
573 573
574 574 config.add_route(
575 575 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
576 576
577 577 # register some exception handling view
578 578 config.add_view(exception_view, context=JSONRPCBaseError)
579 579 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
580 580
581 581 add_api_methods(config)
@@ -1,423 +1,423 b''
1 1 # Copyright (C) 2011-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import logging
20 20 import itertools
21 21 import base64
22 22
23 23 from rhodecode.api import (
24 24 jsonrpc_method, JSONRPCError, JSONRPCForbidden, find_methods)
25 25
26 26 from rhodecode.api.utils import (
27 27 Optional, OAttr, has_superadmin_permission, get_user_or_error)
28 28 from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_repo_store_path
29 29 from rhodecode.lib import system_info
30 30 from rhodecode.lib import user_sessions
31 31 from rhodecode.lib import exc_tracking
32 32 from rhodecode.lib.ext_json import json
33 33 from rhodecode.lib.utils2 import safe_int
34 34 from rhodecode.model.db import UserIpMap
35 35 from rhodecode.model.scm import ScmModel
36 from rhodecode.apps.file_store import utils
36 from rhodecode.apps.file_store import utils as store_utils
37 37 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
38 38 FileOverSizeException
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 @jsonrpc_method()
44 44 def get_server_info(request, apiuser):
45 45 """
46 46 Returns the |RCE| server information.
47 47
48 48 This includes the running version of |RCE| and all installed
49 49 packages. This command takes the following options:
50 50
51 51 :param apiuser: This is filled automatically from the |authtoken|.
52 52 :type apiuser: AuthUser
53 53
54 54 Example output:
55 55
56 56 .. code-block:: bash
57 57
58 58 id : <id_given_in_input>
59 59 result : {
60 60 'modules': [<module name>,...]
61 61 'py_version': <python version>,
62 62 'platform': <platform type>,
63 63 'rhodecode_version': <rhodecode version>
64 64 }
65 65 error : null
66 66 """
67 67
68 68 if not has_superadmin_permission(apiuser):
69 69 raise JSONRPCForbidden()
70 70
71 71 server_info = ScmModel().get_server_info(request.environ)
72 72 # rhodecode-index requires those
73 73
74 74 server_info['index_storage'] = server_info['search']['value']['location']
75 75 server_info['storage'] = server_info['storage']['value']['path']
76 76
77 77 return server_info
78 78
79 79
80 80 @jsonrpc_method()
81 81 def get_repo_store(request, apiuser):
82 82 """
83 83 Returns the |RCE| repository storage information.
84 84
85 85 :param apiuser: This is filled automatically from the |authtoken|.
86 86 :type apiuser: AuthUser
87 87
88 88 Example output:
89 89
90 90 .. code-block:: bash
91 91
92 92 id : <id_given_in_input>
93 93 result : {
94 94 'modules': [<module name>,...]
95 95 'py_version': <python version>,
96 96 'platform': <platform type>,
97 97 'rhodecode_version': <rhodecode version>
98 98 }
99 99 error : null
100 100 """
101 101
102 102 if not has_superadmin_permission(apiuser):
103 103 raise JSONRPCForbidden()
104 104
105 105 path = get_rhodecode_repo_store_path()
106 106 return {"path": path}
107 107
108 108
109 109 @jsonrpc_method()
110 110 def get_ip(request, apiuser, userid=Optional(OAttr('apiuser'))):
111 111 """
112 112 Displays the IP Address as seen from the |RCE| server.
113 113
114 114 * This command displays the IP Address, as well as all the defined IP
115 115 addresses for the specified user. If the ``userid`` is not set, the
116 116 data returned is for the user calling the method.
117 117
118 118 This command can only be run using an |authtoken| with admin rights to
119 119 the specified repository.
120 120
121 121 This command takes the following options:
122 122
123 123 :param apiuser: This is filled automatically from |authtoken|.
124 124 :type apiuser: AuthUser
125 125 :param userid: Sets the userid for which associated IP Address data
126 126 is returned.
127 127 :type userid: Optional(str or int)
128 128
129 129 Example output:
130 130
131 131 .. code-block:: bash
132 132
133 133 id : <id_given_in_input>
134 134 result : {
135 135 "server_ip_addr": "<ip_from_clien>",
136 136 "user_ips": [
137 137 {
138 138 "ip_addr": "<ip_with_mask>",
139 139 "ip_range": ["<start_ip>", "<end_ip>"],
140 140 },
141 141 ...
142 142 ]
143 143 }
144 144
145 145 """
146 146 if not has_superadmin_permission(apiuser):
147 147 raise JSONRPCForbidden()
148 148
149 149 userid = Optional.extract(userid, evaluate_locals=locals())
150 150 userid = getattr(userid, 'user_id', userid)
151 151
152 152 user = get_user_or_error(userid)
153 153 ips = UserIpMap.query().filter(UserIpMap.user == user).all()
154 154 return {
155 155 'server_ip_addr': request.rpc_ip_addr,
156 156 'user_ips': ips
157 157 }
158 158
159 159
160 160 @jsonrpc_method()
161 161 def rescan_repos(request, apiuser, remove_obsolete=Optional(False)):
162 162 """
163 163 Triggers a rescan of the specified repositories.
164 164
165 165 * If the ``remove_obsolete`` option is set, it also deletes repositories
166 166 that are found in the database but not on the file system, so called
167 167 "clean zombies".
168 168
169 169 This command can only be run using an |authtoken| with admin rights to
170 170 the specified repository.
171 171
172 172 This command takes the following options:
173 173
174 174 :param apiuser: This is filled automatically from the |authtoken|.
175 175 :type apiuser: AuthUser
176 176 :param remove_obsolete: Deletes repositories from the database that
177 177 are not found on the filesystem.
178 178 :type remove_obsolete: Optional(``True`` | ``False``)
179 179
180 180 Example output:
181 181
182 182 .. code-block:: bash
183 183
184 184 id : <id_given_in_input>
185 185 result : {
186 186 'added': [<added repository name>,...]
187 187 'removed': [<removed repository name>,...]
188 188 }
189 189 error : null
190 190
191 191 Example error output:
192 192
193 193 .. code-block:: bash
194 194
195 195 id : <id_given_in_input>
196 196 result : null
197 197 error : {
198 198 'Error occurred during rescan repositories action'
199 199 }
200 200
201 201 """
202 202 if not has_superadmin_permission(apiuser):
203 203 raise JSONRPCForbidden()
204 204
205 205 try:
206 206 rm_obsolete = Optional.extract(remove_obsolete)
207 207 added, removed = repo2db_mapper(ScmModel().repo_scan(),
208 208 remove_obsolete=rm_obsolete, force_hooks_rebuild=True)
209 209 return {'added': added, 'removed': removed}
210 210 except Exception:
211 211 log.exception('Failed to run repo rescann')
212 212 raise JSONRPCError(
213 213 'Error occurred during rescan repositories action'
214 214 )
215 215
216 216
217 217 @jsonrpc_method()
218 218 def cleanup_sessions(request, apiuser, older_then=Optional(60)):
219 219 """
220 220 Triggers a session cleanup action.
221 221
222 222 If the ``older_then`` option is set, only sessions that hasn't been
223 223 accessed in the given number of days will be removed.
224 224
225 225 This command can only be run using an |authtoken| with admin rights to
226 226 the specified repository.
227 227
228 228 This command takes the following options:
229 229
230 230 :param apiuser: This is filled automatically from the |authtoken|.
231 231 :type apiuser: AuthUser
232 232 :param older_then: Deletes session that hasn't been accessed
233 233 in given number of days.
234 234 :type older_then: Optional(int)
235 235
236 236 Example output:
237 237
238 238 .. code-block:: bash
239 239
240 240 id : <id_given_in_input>
241 241 result: {
242 242 "backend": "<type of backend>",
243 243 "sessions_removed": <number_of_removed_sessions>
244 244 }
245 245 error : null
246 246
247 247 Example error output:
248 248
249 249 .. code-block:: bash
250 250
251 251 id : <id_given_in_input>
252 252 result : null
253 253 error : {
254 254 'Error occurred during session cleanup'
255 255 }
256 256
257 257 """
258 258 if not has_superadmin_permission(apiuser):
259 259 raise JSONRPCForbidden()
260 260
261 261 older_then = safe_int(Optional.extract(older_then)) or 60
262 262 older_than_seconds = 60 * 60 * 24 * older_then
263 263
264 264 config = system_info.rhodecode_config().get_value()['value']['config']
265 265 session_model = user_sessions.get_session_handler(
266 266 config.get('beaker.session.type', 'memory'))(config)
267 267
268 268 backend = session_model.SESSION_TYPE
269 269 try:
270 270 cleaned = session_model.clean_sessions(
271 271 older_than_seconds=older_than_seconds)
272 272 return {'sessions_removed': cleaned, 'backend': backend}
273 273 except user_sessions.CleanupCommand as msg:
274 274 return {'cleanup_command': str(msg), 'backend': backend}
275 275 except Exception as e:
276 276 log.exception('Failed session cleanup')
277 277 raise JSONRPCError(
278 278 'Error occurred during session cleanup'
279 279 )
280 280
281 281
282 282 @jsonrpc_method()
283 283 def get_method(request, apiuser, pattern=Optional('*')):
284 284 """
285 285 Returns list of all available API methods. By default match pattern
286 286 os "*" but any other pattern can be specified. eg *comment* will return
287 287 all methods with comment inside them. If just single method is matched
288 288 returned data will also include method specification
289 289
290 290 This command can only be run using an |authtoken| with admin rights to
291 291 the specified repository.
292 292
293 293 This command takes the following options:
294 294
295 295 :param apiuser: This is filled automatically from the |authtoken|.
296 296 :type apiuser: AuthUser
297 297 :param pattern: pattern to match method names against
298 298 :type pattern: Optional("*")
299 299
300 300 Example output:
301 301
302 302 .. code-block:: bash
303 303
304 304 id : <id_given_in_input>
305 305 "result": [
306 306 "changeset_comment",
307 307 "comment_pull_request",
308 308 "comment_commit"
309 309 ]
310 310 error : null
311 311
312 312 .. code-block:: bash
313 313
314 314 id : <id_given_in_input>
315 315 "result": [
316 316 "comment_commit",
317 317 {
318 318 "apiuser": "<RequiredType>",
319 319 "comment_type": "<Optional:u'note'>",
320 320 "commit_id": "<RequiredType>",
321 321 "message": "<RequiredType>",
322 322 "repoid": "<RequiredType>",
323 323 "request": "<RequiredType>",
324 324 "resolves_comment_id": "<Optional:None>",
325 325 "status": "<Optional:None>",
326 326 "userid": "<Optional:<OptionalAttr:apiuser>>"
327 327 }
328 328 ]
329 329 error : null
330 330 """
331 from rhodecode.config.patches import inspect_getargspec
332 inspect = inspect_getargspec()
331 from rhodecode.config import patches
332 inspect = patches.inspect_getargspec()
333 333
334 334 if not has_superadmin_permission(apiuser):
335 335 raise JSONRPCForbidden()
336 336
337 337 pattern = Optional.extract(pattern)
338 338
339 339 matches = find_methods(request.registry.jsonrpc_methods, pattern)
340 340
341 341 args_desc = []
342 342 matches_keys = list(matches.keys())
343 343 if len(matches_keys) == 1:
344 344 func = matches[matches_keys[0]]
345 345
346 346 argspec = inspect.getargspec(func)
347 347 arglist = argspec[0]
348 348 defaults = list(map(repr, argspec[3] or []))
349 349
350 350 default_empty = '<RequiredType>'
351 351
352 352 # kw arguments required by this method
353 353 func_kwargs = dict(itertools.zip_longest(
354 354 reversed(arglist), reversed(defaults), fillvalue=default_empty))
355 355 args_desc.append(func_kwargs)
356 356
357 357 return matches_keys + args_desc
358 358
359 359
360 360 @jsonrpc_method()
361 361 def store_exception(request, apiuser, exc_data_json, prefix=Optional('rhodecode')):
362 362 """
363 363 Stores sent exception inside the built-in exception tracker in |RCE| server.
364 364
365 365 This command can only be run using an |authtoken| with admin rights to
366 366 the specified repository.
367 367
368 368 This command takes the following options:
369 369
370 370 :param apiuser: This is filled automatically from the |authtoken|.
371 371 :type apiuser: AuthUser
372 372
373 373 :param exc_data_json: JSON data with exception e.g
374 374 {"exc_traceback": "Value `1` is not allowed", "exc_type_name": "ValueError"}
375 375 :type exc_data_json: JSON data
376 376
377 377 :param prefix: prefix for error type, e.g 'rhodecode', 'vcsserver', 'rhodecode-tools'
378 378 :type prefix: Optional("rhodecode")
379 379
380 380 Example output:
381 381
382 382 .. code-block:: bash
383 383
384 384 id : <id_given_in_input>
385 385 "result": {
386 386 "exc_id": 139718459226384,
387 387 "exc_url": "http://localhost:8080/_admin/settings/exceptions/139718459226384"
388 388 }
389 389 error : null
390 390 """
391 391 if not has_superadmin_permission(apiuser):
392 392 raise JSONRPCForbidden()
393 393
394 394 prefix = Optional.extract(prefix)
395 395 exc_id = exc_tracking.generate_id()
396 396
397 397 try:
398 398 exc_data = json.loads(exc_data_json)
399 399 except Exception:
400 400 log.error('Failed to parse JSON: %r', exc_data_json)
401 401 raise JSONRPCError('Failed to parse JSON data from exc_data_json field. '
402 402 'Please make sure it contains a valid JSON.')
403 403
404 404 try:
405 405 exc_traceback = exc_data['exc_traceback']
406 406 exc_type_name = exc_data['exc_type_name']
407 407 exc_value = ''
408 408 except KeyError as err:
409 409 raise JSONRPCError(
410 410 f'Missing exc_traceback, or exc_type_name '
411 411 f'in exc_data_json field. Missing: {err}')
412 412
413 413 class ExcType:
414 414 __name__ = exc_type_name
415 415
416 416 exc_info = (ExcType(), exc_value, exc_traceback)
417 417
418 418 exc_tracking._store_exception(
419 419 exc_id=exc_id, exc_info=exc_info, prefix=prefix)
420 420
421 421 exc_url = request.route_url(
422 422 'admin_settings_exception_tracker_show', exception_id=exc_id)
423 423 return {'exc_id': exc_id, 'exc_url': exc_url}
@@ -1,1093 +1,1124 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 from rhodecode.apps._base import ADMIN_PREFIX
21 21 from rhodecode.apps._base.navigation import includeme as nav_includeme
22 22 from rhodecode.apps.admin.views.main_views import AdminMainView
23 23
24 24
25 25 def admin_routes(config):
26 26 """
27 27 Admin prefixed routes
28 28 """
29 29 from rhodecode.apps.admin.views.audit_logs import AdminAuditLogsView
30 30 from rhodecode.apps.admin.views.artifacts import AdminArtifactsView
31 31 from rhodecode.apps.admin.views.automation import AdminAutomationView
32 32 from rhodecode.apps.admin.views.scheduler import AdminSchedulerView
33 33 from rhodecode.apps.admin.views.defaults import AdminDefaultSettingsView
34 34 from rhodecode.apps.admin.views.exception_tracker import ExceptionsTrackerView
35 35 from rhodecode.apps.admin.views.open_source_licenses import OpenSourceLicensesAdminSettingsView
36 36 from rhodecode.apps.admin.views.permissions import AdminPermissionsView
37 37 from rhodecode.apps.admin.views.process_management import AdminProcessManagementView
38 38 from rhodecode.apps.admin.views.repo_groups import AdminRepoGroupsView
39 39 from rhodecode.apps.admin.views.repositories import AdminReposView
40 40 from rhodecode.apps.admin.views.sessions import AdminSessionSettingsView
41 41 from rhodecode.apps.admin.views.settings import AdminSettingsView
42 42 from rhodecode.apps.admin.views.svn_config import AdminSvnConfigView
43 43 from rhodecode.apps.admin.views.system_info import AdminSystemInfoSettingsView
44 44 from rhodecode.apps.admin.views.user_groups import AdminUserGroupsView
45 45 from rhodecode.apps.admin.views.users import AdminUsersView, UsersView
46
46 from rhodecode.apps.admin.views.security import AdminSecurityView
47
48 # Security EE feature
49
50 config.add_route(
51 'admin_security',
52 pattern='/security')
53 config.add_view(
54 AdminSecurityView,
55 attr='security',
56 route_name='admin_security', request_method='GET',
57 renderer='rhodecode:templates/admin/security/security.mako')
58
59 config.add_route(
60 name='admin_security_update',
61 pattern='/security/update')
62 config.add_view(
63 AdminSecurityView,
64 attr='security_update',
65 route_name='admin_security_update', request_method='POST',
66 renderer='rhodecode:templates/admin/security/security.mako')
67
68 config.add_route(
69 name='admin_security_modify_allowed_vcs_client_versions',
70 pattern=ADMIN_PREFIX + '/security/modify/allowed_vcs_client_versions')
71 config.add_view(
72 AdminSecurityView,
73 attr='vcs_whitelisted_client_versions_edit',
74 route_name='admin_security_modify_allowed_vcs_client_versions', request_method=('GET', 'POST'),
75 renderer='rhodecode:templates/admin/security/edit_allowed_vcs_client_versions.mako')
76
77
47 78 config.add_route(
48 79 name='admin_audit_logs',
49 80 pattern='/audit_logs')
50 81 config.add_view(
51 82 AdminAuditLogsView,
52 83 attr='admin_audit_logs',
53 84 route_name='admin_audit_logs', request_method='GET',
54 85 renderer='rhodecode:templates/admin/admin_audit_logs.mako')
55 86
56 87 config.add_route(
57 88 name='admin_audit_log_entry',
58 89 pattern='/audit_logs/{audit_log_id}')
59 90 config.add_view(
60 91 AdminAuditLogsView,
61 92 attr='admin_audit_log_entry',
62 93 route_name='admin_audit_log_entry', request_method='GET',
63 94 renderer='rhodecode:templates/admin/admin_audit_log_entry.mako')
64 95
65 96 # Artifacts EE feature
66 97 config.add_route(
67 98 'admin_artifacts',
68 99 pattern=ADMIN_PREFIX + '/artifacts')
69 100 config.add_route(
70 101 'admin_artifacts_show_all',
71 102 pattern=ADMIN_PREFIX + '/artifacts')
72 103 config.add_view(
73 104 AdminArtifactsView,
74 105 attr='artifacts',
75 106 route_name='admin_artifacts', request_method='GET',
76 107 renderer='rhodecode:templates/admin/artifacts/artifacts.mako')
77 108 config.add_view(
78 109 AdminArtifactsView,
79 110 attr='artifacts',
80 111 route_name='admin_artifacts_show_all', request_method='GET',
81 112 renderer='rhodecode:templates/admin/artifacts/artifacts.mako')
82 113
83 114 # EE views
84 115 config.add_route(
85 116 name='admin_artifacts_show_info',
86 117 pattern=ADMIN_PREFIX + '/artifacts/{uid}')
87 118 config.add_route(
88 119 name='admin_artifacts_delete',
89 120 pattern=ADMIN_PREFIX + '/artifacts/{uid}/delete')
90 121 config.add_route(
91 122 name='admin_artifacts_update',
92 123 pattern=ADMIN_PREFIX + '/artifacts/{uid}/update')
93 124
94 125 # Automation EE feature
95 126 config.add_route(
96 127 'admin_automation',
97 128 pattern=ADMIN_PREFIX + '/automation')
98 129 config.add_view(
99 130 AdminAutomationView,
100 131 attr='automation',
101 132 route_name='admin_automation', request_method='GET',
102 133 renderer='rhodecode:templates/admin/automation/automation.mako')
103 134
104 135 # Scheduler EE feature
105 136 config.add_route(
106 137 'admin_scheduler',
107 138 pattern=ADMIN_PREFIX + '/scheduler')
108 139 config.add_view(
109 140 AdminSchedulerView,
110 141 attr='scheduler',
111 142 route_name='admin_scheduler', request_method='GET',
112 143 renderer='rhodecode:templates/admin/scheduler/scheduler.mako')
113 144
114 145 config.add_route(
115 146 name='admin_settings_open_source',
116 147 pattern='/settings/open_source')
117 148 config.add_view(
118 149 OpenSourceLicensesAdminSettingsView,
119 150 attr='open_source_licenses',
120 151 route_name='admin_settings_open_source', request_method='GET',
121 152 renderer='rhodecode:templates/admin/settings/settings.mako')
122 153
123 154 config.add_route(
124 155 name='admin_settings_vcs_svn_generate_cfg',
125 156 pattern='/settings/vcs/svn_generate_cfg')
126 157 config.add_view(
127 158 AdminSvnConfigView,
128 159 attr='vcs_svn_generate_config',
129 160 route_name='admin_settings_vcs_svn_generate_cfg',
130 161 request_method='POST', renderer='json')
131 162
132 163 config.add_route(
133 164 name='admin_settings_system',
134 165 pattern='/settings/system')
135 166 config.add_view(
136 167 AdminSystemInfoSettingsView,
137 168 attr='settings_system_info',
138 169 route_name='admin_settings_system', request_method='GET',
139 170 renderer='rhodecode:templates/admin/settings/settings.mako')
140 171
141 172 config.add_route(
142 173 name='admin_settings_system_update',
143 174 pattern='/settings/system/updates')
144 175 config.add_view(
145 176 AdminSystemInfoSettingsView,
146 177 attr='settings_system_info_check_update',
147 178 route_name='admin_settings_system_update', request_method='GET',
148 179 renderer='rhodecode:templates/admin/settings/settings_system_update.mako')
149 180
150 181 config.add_route(
151 182 name='admin_settings_exception_tracker',
152 183 pattern='/settings/exceptions')
153 184 config.add_view(
154 185 ExceptionsTrackerView,
155 186 attr='browse_exceptions',
156 187 route_name='admin_settings_exception_tracker', request_method='GET',
157 188 renderer='rhodecode:templates/admin/settings/settings.mako')
158 189
159 190 config.add_route(
160 191 name='admin_settings_exception_tracker_delete_all',
161 192 pattern='/settings/exceptions_delete_all')
162 193 config.add_view(
163 194 ExceptionsTrackerView,
164 195 attr='exception_delete_all',
165 196 route_name='admin_settings_exception_tracker_delete_all', request_method='POST',
166 197 renderer='rhodecode:templates/admin/settings/settings.mako')
167 198
168 199 config.add_route(
169 200 name='admin_settings_exception_tracker_show',
170 201 pattern='/settings/exceptions/{exception_id}')
171 202 config.add_view(
172 203 ExceptionsTrackerView,
173 204 attr='exception_show',
174 205 route_name='admin_settings_exception_tracker_show', request_method='GET',
175 206 renderer='rhodecode:templates/admin/settings/settings.mako')
176 207
177 208 config.add_route(
178 209 name='admin_settings_exception_tracker_delete',
179 210 pattern='/settings/exceptions/{exception_id}/delete')
180 211 config.add_view(
181 212 ExceptionsTrackerView,
182 213 attr='exception_delete',
183 214 route_name='admin_settings_exception_tracker_delete', request_method='POST',
184 215 renderer='rhodecode:templates/admin/settings/settings.mako')
185 216
186 217 config.add_route(
187 218 name='admin_settings_sessions',
188 219 pattern='/settings/sessions')
189 220 config.add_view(
190 221 AdminSessionSettingsView,
191 222 attr='settings_sessions',
192 223 route_name='admin_settings_sessions', request_method='GET',
193 224 renderer='rhodecode:templates/admin/settings/settings.mako')
194 225
195 226 config.add_route(
196 227 name='admin_settings_sessions_cleanup',
197 228 pattern='/settings/sessions/cleanup')
198 229 config.add_view(
199 230 AdminSessionSettingsView,
200 231 attr='settings_sessions_cleanup',
201 232 route_name='admin_settings_sessions_cleanup', request_method='POST')
202 233
203 234 config.add_route(
204 235 name='admin_settings_process_management',
205 236 pattern='/settings/process_management')
206 237 config.add_view(
207 238 AdminProcessManagementView,
208 239 attr='process_management',
209 240 route_name='admin_settings_process_management', request_method='GET',
210 241 renderer='rhodecode:templates/admin/settings/settings.mako')
211 242
212 243 config.add_route(
213 244 name='admin_settings_process_management_data',
214 245 pattern='/settings/process_management/data')
215 246 config.add_view(
216 247 AdminProcessManagementView,
217 248 attr='process_management_data',
218 249 route_name='admin_settings_process_management_data', request_method='GET',
219 250 renderer='rhodecode:templates/admin/settings/settings_process_management_data.mako')
220 251
221 252 config.add_route(
222 253 name='admin_settings_process_management_signal',
223 254 pattern='/settings/process_management/signal')
224 255 config.add_view(
225 256 AdminProcessManagementView,
226 257 attr='process_management_signal',
227 258 route_name='admin_settings_process_management_signal',
228 259 request_method='POST', renderer='json_ext')
229 260
230 261 config.add_route(
231 262 name='admin_settings_process_management_master_signal',
232 263 pattern='/settings/process_management/master_signal')
233 264 config.add_view(
234 265 AdminProcessManagementView,
235 266 attr='process_management_master_signal',
236 267 route_name='admin_settings_process_management_master_signal',
237 268 request_method='POST', renderer='json_ext')
238 269
239 270 # default settings
240 271 config.add_route(
241 272 name='admin_defaults_repositories',
242 273 pattern='/defaults/repositories')
243 274 config.add_view(
244 275 AdminDefaultSettingsView,
245 276 attr='defaults_repository_show',
246 277 route_name='admin_defaults_repositories', request_method='GET',
247 278 renderer='rhodecode:templates/admin/defaults/defaults.mako')
248 279
249 280 config.add_route(
250 281 name='admin_defaults_repositories_update',
251 282 pattern='/defaults/repositories/update')
252 283 config.add_view(
253 284 AdminDefaultSettingsView,
254 285 attr='defaults_repository_update',
255 286 route_name='admin_defaults_repositories_update', request_method='POST',
256 287 renderer='rhodecode:templates/admin/defaults/defaults.mako')
257 288
258 289 # admin settings
259 290
260 291 config.add_route(
261 292 name='admin_settings',
262 293 pattern='/settings')
263 294 config.add_view(
264 295 AdminSettingsView,
265 296 attr='settings_global',
266 297 route_name='admin_settings', request_method='GET',
267 298 renderer='rhodecode:templates/admin/settings/settings.mako')
268 299
269 300 config.add_route(
270 301 name='admin_settings_update',
271 302 pattern='/settings/update')
272 303 config.add_view(
273 304 AdminSettingsView,
274 305 attr='settings_global_update',
275 306 route_name='admin_settings_update', request_method='POST',
276 307 renderer='rhodecode:templates/admin/settings/settings.mako')
277 308
278 309 config.add_route(
279 310 name='admin_settings_global',
280 311 pattern='/settings/global')
281 312 config.add_view(
282 313 AdminSettingsView,
283 314 attr='settings_global',
284 315 route_name='admin_settings_global', request_method='GET',
285 316 renderer='rhodecode:templates/admin/settings/settings.mako')
286 317
287 318 config.add_route(
288 319 name='admin_settings_global_update',
289 320 pattern='/settings/global/update')
290 321 config.add_view(
291 322 AdminSettingsView,
292 323 attr='settings_global_update',
293 324 route_name='admin_settings_global_update', request_method='POST',
294 325 renderer='rhodecode:templates/admin/settings/settings.mako')
295 326
296 327 config.add_route(
297 328 name='admin_settings_vcs',
298 329 pattern='/settings/vcs')
299 330 config.add_view(
300 331 AdminSettingsView,
301 332 attr='settings_vcs',
302 333 route_name='admin_settings_vcs', request_method='GET',
303 334 renderer='rhodecode:templates/admin/settings/settings.mako')
304 335
305 336 config.add_route(
306 337 name='admin_settings_vcs_update',
307 338 pattern='/settings/vcs/update')
308 339 config.add_view(
309 340 AdminSettingsView,
310 341 attr='settings_vcs_update',
311 342 route_name='admin_settings_vcs_update', request_method='POST',
312 343 renderer='rhodecode:templates/admin/settings/settings.mako')
313 344
314 345 config.add_route(
315 346 name='admin_settings_vcs_svn_pattern_delete',
316 347 pattern='/settings/vcs/svn_pattern_delete')
317 348 config.add_view(
318 349 AdminSettingsView,
319 350 attr='settings_vcs_delete_svn_pattern',
320 351 route_name='admin_settings_vcs_svn_pattern_delete', request_method='POST',
321 352 renderer='json_ext', xhr=True)
322 353
323 354 config.add_route(
324 355 name='admin_settings_mapping',
325 356 pattern='/settings/mapping')
326 357 config.add_view(
327 358 AdminSettingsView,
328 359 attr='settings_mapping',
329 360 route_name='admin_settings_mapping', request_method='GET',
330 361 renderer='rhodecode:templates/admin/settings/settings.mako')
331 362
332 363 config.add_route(
333 364 name='admin_settings_mapping_update',
334 365 pattern='/settings/mapping/update')
335 366 config.add_view(
336 367 AdminSettingsView,
337 368 attr='settings_mapping_update',
338 369 route_name='admin_settings_mapping_update', request_method='POST',
339 370 renderer='rhodecode:templates/admin/settings/settings.mako')
340 371
341 372 config.add_route(
342 373 name='admin_settings_visual',
343 374 pattern='/settings/visual')
344 375 config.add_view(
345 376 AdminSettingsView,
346 377 attr='settings_visual',
347 378 route_name='admin_settings_visual', request_method='GET',
348 379 renderer='rhodecode:templates/admin/settings/settings.mako')
349 380
350 381 config.add_route(
351 382 name='admin_settings_visual_update',
352 383 pattern='/settings/visual/update')
353 384 config.add_view(
354 385 AdminSettingsView,
355 386 attr='settings_visual_update',
356 387 route_name='admin_settings_visual_update', request_method='POST',
357 388 renderer='rhodecode:templates/admin/settings/settings.mako')
358 389
359 390 config.add_route(
360 391 name='admin_settings_issuetracker',
361 392 pattern='/settings/issue-tracker')
362 393 config.add_view(
363 394 AdminSettingsView,
364 395 attr='settings_issuetracker',
365 396 route_name='admin_settings_issuetracker', request_method='GET',
366 397 renderer='rhodecode:templates/admin/settings/settings.mako')
367 398
368 399 config.add_route(
369 400 name='admin_settings_issuetracker_update',
370 401 pattern='/settings/issue-tracker/update')
371 402 config.add_view(
372 403 AdminSettingsView,
373 404 attr='settings_issuetracker_update',
374 405 route_name='admin_settings_issuetracker_update', request_method='POST',
375 406 renderer='rhodecode:templates/admin/settings/settings.mako')
376 407
377 408 config.add_route(
378 409 name='admin_settings_issuetracker_test',
379 410 pattern='/settings/issue-tracker/test')
380 411 config.add_view(
381 412 AdminSettingsView,
382 413 attr='settings_issuetracker_test',
383 414 route_name='admin_settings_issuetracker_test', request_method='POST',
384 415 renderer='string', xhr=True)
385 416
386 417 config.add_route(
387 418 name='admin_settings_issuetracker_delete',
388 419 pattern='/settings/issue-tracker/delete')
389 420 config.add_view(
390 421 AdminSettingsView,
391 422 attr='settings_issuetracker_delete',
392 423 route_name='admin_settings_issuetracker_delete', request_method='POST',
393 424 renderer='json_ext', xhr=True)
394 425
395 426 config.add_route(
396 427 name='admin_settings_email',
397 428 pattern='/settings/email')
398 429 config.add_view(
399 430 AdminSettingsView,
400 431 attr='settings_email',
401 432 route_name='admin_settings_email', request_method='GET',
402 433 renderer='rhodecode:templates/admin/settings/settings.mako')
403 434
404 435 config.add_route(
405 436 name='admin_settings_email_update',
406 437 pattern='/settings/email/update')
407 438 config.add_view(
408 439 AdminSettingsView,
409 440 attr='settings_email_update',
410 441 route_name='admin_settings_email_update', request_method='POST',
411 442 renderer='rhodecode:templates/admin/settings/settings.mako')
412 443
413 444 config.add_route(
414 445 name='admin_settings_hooks',
415 446 pattern='/settings/hooks')
416 447 config.add_view(
417 448 AdminSettingsView,
418 449 attr='settings_hooks',
419 450 route_name='admin_settings_hooks', request_method='GET',
420 451 renderer='rhodecode:templates/admin/settings/settings.mako')
421 452
422 453 config.add_route(
423 454 name='admin_settings_hooks_update',
424 455 pattern='/settings/hooks/update')
425 456 config.add_view(
426 457 AdminSettingsView,
427 458 attr='settings_hooks_update',
428 459 route_name='admin_settings_hooks_update', request_method='POST',
429 460 renderer='rhodecode:templates/admin/settings/settings.mako')
430 461
431 462 config.add_route(
432 463 name='admin_settings_hooks_delete',
433 464 pattern='/settings/hooks/delete')
434 465 config.add_view(
435 466 AdminSettingsView,
436 467 attr='settings_hooks_update',
437 468 route_name='admin_settings_hooks_delete', request_method='POST',
438 469 renderer='rhodecode:templates/admin/settings/settings.mako')
439 470
440 471 config.add_route(
441 472 name='admin_settings_search',
442 473 pattern='/settings/search')
443 474 config.add_view(
444 475 AdminSettingsView,
445 476 attr='settings_search',
446 477 route_name='admin_settings_search', request_method='GET',
447 478 renderer='rhodecode:templates/admin/settings/settings.mako')
448 479
449 480 config.add_route(
450 481 name='admin_settings_labs',
451 482 pattern='/settings/labs')
452 483 config.add_view(
453 484 AdminSettingsView,
454 485 attr='settings_labs',
455 486 route_name='admin_settings_labs', request_method='GET',
456 487 renderer='rhodecode:templates/admin/settings/settings.mako')
457 488
458 489 config.add_route(
459 490 name='admin_settings_labs_update',
460 491 pattern='/settings/labs/update')
461 492 config.add_view(
462 493 AdminSettingsView,
463 494 attr='settings_labs_update',
464 495 route_name='admin_settings_labs_update', request_method='POST',
465 496 renderer='rhodecode:templates/admin/settings/settings.mako')
466 497
467 498 # global permissions
468 499
469 500 config.add_route(
470 501 name='admin_permissions_application',
471 502 pattern='/permissions/application')
472 503 config.add_view(
473 504 AdminPermissionsView,
474 505 attr='permissions_application',
475 506 route_name='admin_permissions_application', request_method='GET',
476 507 renderer='rhodecode:templates/admin/permissions/permissions.mako')
477 508
478 509 config.add_route(
479 510 name='admin_permissions_application_update',
480 511 pattern='/permissions/application/update')
481 512 config.add_view(
482 513 AdminPermissionsView,
483 514 attr='permissions_application_update',
484 515 route_name='admin_permissions_application_update', request_method='POST',
485 516 renderer='rhodecode:templates/admin/permissions/permissions.mako')
486 517
487 518 config.add_route(
488 519 name='admin_permissions_global',
489 520 pattern='/permissions/global')
490 521 config.add_view(
491 522 AdminPermissionsView,
492 523 attr='permissions_global',
493 524 route_name='admin_permissions_global', request_method='GET',
494 525 renderer='rhodecode:templates/admin/permissions/permissions.mako')
495 526
496 527 config.add_route(
497 528 name='admin_permissions_global_update',
498 529 pattern='/permissions/global/update')
499 530 config.add_view(
500 531 AdminPermissionsView,
501 532 attr='permissions_global_update',
502 533 route_name='admin_permissions_global_update', request_method='POST',
503 534 renderer='rhodecode:templates/admin/permissions/permissions.mako')
504 535
505 536 config.add_route(
506 537 name='admin_permissions_object',
507 538 pattern='/permissions/object')
508 539 config.add_view(
509 540 AdminPermissionsView,
510 541 attr='permissions_objects',
511 542 route_name='admin_permissions_object', request_method='GET',
512 543 renderer='rhodecode:templates/admin/permissions/permissions.mako')
513 544
514 545 config.add_route(
515 546 name='admin_permissions_object_update',
516 547 pattern='/permissions/object/update')
517 548 config.add_view(
518 549 AdminPermissionsView,
519 550 attr='permissions_objects_update',
520 551 route_name='admin_permissions_object_update', request_method='POST',
521 552 renderer='rhodecode:templates/admin/permissions/permissions.mako')
522 553
523 554 # Branch perms EE feature
524 555 config.add_route(
525 556 name='admin_permissions_branch',
526 557 pattern='/permissions/branch')
527 558 config.add_view(
528 559 AdminPermissionsView,
529 560 attr='permissions_branch',
530 561 route_name='admin_permissions_branch', request_method='GET',
531 562 renderer='rhodecode:templates/admin/permissions/permissions.mako')
532 563
533 564 config.add_route(
534 565 name='admin_permissions_ips',
535 566 pattern='/permissions/ips')
536 567 config.add_view(
537 568 AdminPermissionsView,
538 569 attr='permissions_ips',
539 570 route_name='admin_permissions_ips', request_method='GET',
540 571 renderer='rhodecode:templates/admin/permissions/permissions.mako')
541 572
542 573 config.add_route(
543 574 name='admin_permissions_overview',
544 575 pattern='/permissions/overview')
545 576 config.add_view(
546 577 AdminPermissionsView,
547 578 attr='permissions_overview',
548 579 route_name='admin_permissions_overview', request_method='GET',
549 580 renderer='rhodecode:templates/admin/permissions/permissions.mako')
550 581
551 582 config.add_route(
552 583 name='admin_permissions_auth_token_access',
553 584 pattern='/permissions/auth_token_access')
554 585 config.add_view(
555 586 AdminPermissionsView,
556 587 attr='auth_token_access',
557 588 route_name='admin_permissions_auth_token_access', request_method='GET',
558 589 renderer='rhodecode:templates/admin/permissions/permissions.mako')
559 590
560 591 config.add_route(
561 592 name='admin_permissions_ssh_keys',
562 593 pattern='/permissions/ssh_keys')
563 594 config.add_view(
564 595 AdminPermissionsView,
565 596 attr='ssh_keys',
566 597 route_name='admin_permissions_ssh_keys', request_method='GET',
567 598 renderer='rhodecode:templates/admin/permissions/permissions.mako')
568 599
569 600 config.add_route(
570 601 name='admin_permissions_ssh_keys_data',
571 602 pattern='/permissions/ssh_keys/data')
572 603 config.add_view(
573 604 AdminPermissionsView,
574 605 attr='ssh_keys_data',
575 606 route_name='admin_permissions_ssh_keys_data', request_method='GET',
576 607 renderer='json_ext', xhr=True)
577 608
578 609 config.add_route(
579 610 name='admin_permissions_ssh_keys_update',
580 611 pattern='/permissions/ssh_keys/update')
581 612 config.add_view(
582 613 AdminPermissionsView,
583 614 attr='ssh_keys_update',
584 615 route_name='admin_permissions_ssh_keys_update', request_method='POST',
585 616 renderer='rhodecode:templates/admin/permissions/permissions.mako')
586 617
587 618 # users admin
588 619 config.add_route(
589 620 name='users',
590 621 pattern='/users')
591 622 config.add_view(
592 623 AdminUsersView,
593 624 attr='users_list',
594 625 route_name='users', request_method='GET',
595 626 renderer='rhodecode:templates/admin/users/users.mako')
596 627
597 628 config.add_route(
598 629 name='users_data',
599 630 pattern='/users_data')
600 631 config.add_view(
601 632 AdminUsersView,
602 633 attr='users_list_data',
603 634 # renderer defined below
604 635 route_name='users_data', request_method='GET',
605 636 renderer='json_ext', xhr=True)
606 637
607 638 config.add_route(
608 639 name='users_create',
609 640 pattern='/users/create')
610 641 config.add_view(
611 642 AdminUsersView,
612 643 attr='users_create',
613 644 route_name='users_create', request_method='POST',
614 645 renderer='rhodecode:templates/admin/users/user_add.mako')
615 646
616 647 config.add_route(
617 648 name='users_new',
618 649 pattern='/users/new')
619 650 config.add_view(
620 651 AdminUsersView,
621 652 attr='users_new',
622 653 route_name='users_new', request_method='GET',
623 654 renderer='rhodecode:templates/admin/users/user_add.mako')
624 655
625 656 # user management
626 657 config.add_route(
627 658 name='user_edit',
628 659 pattern=r'/users/{user_id:\d+}/edit',
629 660 user_route=True)
630 661 config.add_view(
631 662 UsersView,
632 663 attr='user_edit',
633 664 route_name='user_edit', request_method='GET',
634 665 renderer='rhodecode:templates/admin/users/user_edit.mako')
635 666
636 667 config.add_route(
637 668 name='user_edit_advanced',
638 669 pattern=r'/users/{user_id:\d+}/edit/advanced',
639 670 user_route=True)
640 671 config.add_view(
641 672 UsersView,
642 673 attr='user_edit_advanced',
643 674 route_name='user_edit_advanced', request_method='GET',
644 675 renderer='rhodecode:templates/admin/users/user_edit.mako')
645 676
646 677 config.add_route(
647 678 name='user_edit_global_perms',
648 679 pattern=r'/users/{user_id:\d+}/edit/global_permissions',
649 680 user_route=True)
650 681 config.add_view(
651 682 UsersView,
652 683 attr='user_edit_global_perms',
653 684 route_name='user_edit_global_perms', request_method='GET',
654 685 renderer='rhodecode:templates/admin/users/user_edit.mako')
655 686
656 687 config.add_route(
657 688 name='user_edit_global_perms_update',
658 689 pattern=r'/users/{user_id:\d+}/edit/global_permissions/update',
659 690 user_route=True)
660 691 config.add_view(
661 692 UsersView,
662 693 attr='user_edit_global_perms_update',
663 694 route_name='user_edit_global_perms_update', request_method='POST',
664 695 renderer='rhodecode:templates/admin/users/user_edit.mako')
665 696
666 697 config.add_route(
667 698 name='user_update',
668 699 pattern=r'/users/{user_id:\d+}/update',
669 700 user_route=True)
670 701 config.add_view(
671 702 UsersView,
672 703 attr='user_update',
673 704 route_name='user_update', request_method='POST',
674 705 renderer='rhodecode:templates/admin/users/user_edit.mako')
675 706
676 707 config.add_route(
677 708 name='user_delete',
678 709 pattern=r'/users/{user_id:\d+}/delete',
679 710 user_route=True)
680 711 config.add_view(
681 712 UsersView,
682 713 attr='user_delete',
683 714 route_name='user_delete', request_method='POST',
684 715 renderer='rhodecode:templates/admin/users/user_edit.mako')
685 716
686 717 config.add_route(
687 718 name='user_enable_force_password_reset',
688 719 pattern=r'/users/{user_id:\d+}/password_reset_enable',
689 720 user_route=True)
690 721 config.add_view(
691 722 UsersView,
692 723 attr='user_enable_force_password_reset',
693 724 route_name='user_enable_force_password_reset', request_method='POST',
694 725 renderer='rhodecode:templates/admin/users/user_edit.mako')
695 726
696 727 config.add_route(
697 728 name='user_disable_force_password_reset',
698 729 pattern=r'/users/{user_id:\d+}/password_reset_disable',
699 730 user_route=True)
700 731 config.add_view(
701 732 UsersView,
702 733 attr='user_disable_force_password_reset',
703 734 route_name='user_disable_force_password_reset', request_method='POST',
704 735 renderer='rhodecode:templates/admin/users/user_edit.mako')
705 736
706 737 config.add_route(
707 738 name='user_create_personal_repo_group',
708 739 pattern=r'/users/{user_id:\d+}/create_repo_group',
709 740 user_route=True)
710 741 config.add_view(
711 742 UsersView,
712 743 attr='user_create_personal_repo_group',
713 744 route_name='user_create_personal_repo_group', request_method='POST',
714 745 renderer='rhodecode:templates/admin/users/user_edit.mako')
715 746
716 747 # user notice
717 748 config.add_route(
718 749 name='user_notice_dismiss',
719 750 pattern=r'/users/{user_id:\d+}/notice_dismiss',
720 751 user_route=True)
721 752 config.add_view(
722 753 UsersView,
723 754 attr='user_notice_dismiss',
724 755 route_name='user_notice_dismiss', request_method='POST',
725 756 renderer='json_ext', xhr=True)
726 757
727 758 # user auth tokens
728 759 config.add_route(
729 760 name='edit_user_auth_tokens',
730 761 pattern=r'/users/{user_id:\d+}/edit/auth_tokens',
731 762 user_route=True)
732 763 config.add_view(
733 764 UsersView,
734 765 attr='auth_tokens',
735 766 route_name='edit_user_auth_tokens', request_method='GET',
736 767 renderer='rhodecode:templates/admin/users/user_edit.mako')
737 768
738 769 config.add_route(
739 770 name='edit_user_auth_tokens_view',
740 771 pattern=r'/users/{user_id:\d+}/edit/auth_tokens/view',
741 772 user_route=True)
742 773 config.add_view(
743 774 UsersView,
744 775 attr='auth_tokens_view',
745 776 route_name='edit_user_auth_tokens_view', request_method='POST',
746 777 renderer='json_ext', xhr=True)
747 778
748 779 config.add_route(
749 780 name='edit_user_auth_tokens_add',
750 781 pattern=r'/users/{user_id:\d+}/edit/auth_tokens/new',
751 782 user_route=True)
752 783 config.add_view(
753 784 UsersView,
754 785 attr='auth_tokens_add',
755 786 route_name='edit_user_auth_tokens_add', request_method='POST')
756 787
757 788 config.add_route(
758 789 name='edit_user_auth_tokens_delete',
759 790 pattern=r'/users/{user_id:\d+}/edit/auth_tokens/delete',
760 791 user_route=True)
761 792 config.add_view(
762 793 UsersView,
763 794 attr='auth_tokens_delete',
764 795 route_name='edit_user_auth_tokens_delete', request_method='POST')
765 796
766 797 # user ssh keys
767 798 config.add_route(
768 799 name='edit_user_ssh_keys',
769 800 pattern=r'/users/{user_id:\d+}/edit/ssh_keys',
770 801 user_route=True)
771 802 config.add_view(
772 803 UsersView,
773 804 attr='ssh_keys',
774 805 route_name='edit_user_ssh_keys', request_method='GET',
775 806 renderer='rhodecode:templates/admin/users/user_edit.mako')
776 807
777 808 config.add_route(
778 809 name='edit_user_ssh_keys_generate_keypair',
779 810 pattern=r'/users/{user_id:\d+}/edit/ssh_keys/generate',
780 811 user_route=True)
781 812 config.add_view(
782 813 UsersView,
783 814 attr='ssh_keys_generate_keypair',
784 815 route_name='edit_user_ssh_keys_generate_keypair', request_method='GET',
785 816 renderer='rhodecode:templates/admin/users/user_edit.mako')
786 817
787 818 config.add_route(
788 819 name='edit_user_ssh_keys_add',
789 820 pattern=r'/users/{user_id:\d+}/edit/ssh_keys/new',
790 821 user_route=True)
791 822 config.add_view(
792 823 UsersView,
793 824 attr='ssh_keys_add',
794 825 route_name='edit_user_ssh_keys_add', request_method='POST')
795 826
796 827 config.add_route(
797 828 name='edit_user_ssh_keys_delete',
798 829 pattern=r'/users/{user_id:\d+}/edit/ssh_keys/delete',
799 830 user_route=True)
800 831 config.add_view(
801 832 UsersView,
802 833 attr='ssh_keys_delete',
803 834 route_name='edit_user_ssh_keys_delete', request_method='POST')
804 835
805 836 # user emails
806 837 config.add_route(
807 838 name='edit_user_emails',
808 839 pattern=r'/users/{user_id:\d+}/edit/emails',
809 840 user_route=True)
810 841 config.add_view(
811 842 UsersView,
812 843 attr='emails',
813 844 route_name='edit_user_emails', request_method='GET',
814 845 renderer='rhodecode:templates/admin/users/user_edit.mako')
815 846
816 847 config.add_route(
817 848 name='edit_user_emails_add',
818 849 pattern=r'/users/{user_id:\d+}/edit/emails/new',
819 850 user_route=True)
820 851 config.add_view(
821 852 UsersView,
822 853 attr='emails_add',
823 854 route_name='edit_user_emails_add', request_method='POST')
824 855
825 856 config.add_route(
826 857 name='edit_user_emails_delete',
827 858 pattern=r'/users/{user_id:\d+}/edit/emails/delete',
828 859 user_route=True)
829 860 config.add_view(
830 861 UsersView,
831 862 attr='emails_delete',
832 863 route_name='edit_user_emails_delete', request_method='POST')
833 864
834 865 # user IPs
835 866 config.add_route(
836 867 name='edit_user_ips',
837 868 pattern=r'/users/{user_id:\d+}/edit/ips',
838 869 user_route=True)
839 870 config.add_view(
840 871 UsersView,
841 872 attr='ips',
842 873 route_name='edit_user_ips', request_method='GET',
843 874 renderer='rhodecode:templates/admin/users/user_edit.mako')
844 875
845 876 config.add_route(
846 877 name='edit_user_ips_add',
847 878 pattern=r'/users/{user_id:\d+}/edit/ips/new',
848 879 user_route_with_default=True) # enabled for default user too
849 880 config.add_view(
850 881 UsersView,
851 882 attr='ips_add',
852 883 route_name='edit_user_ips_add', request_method='POST')
853 884
854 885 config.add_route(
855 886 name='edit_user_ips_delete',
856 887 pattern=r'/users/{user_id:\d+}/edit/ips/delete',
857 888 user_route_with_default=True) # enabled for default user too
858 889 config.add_view(
859 890 UsersView,
860 891 attr='ips_delete',
861 892 route_name='edit_user_ips_delete', request_method='POST')
862 893
863 894 # user perms
864 895 config.add_route(
865 896 name='edit_user_perms_summary',
866 897 pattern=r'/users/{user_id:\d+}/edit/permissions_summary',
867 898 user_route=True)
868 899 config.add_view(
869 900 UsersView,
870 901 attr='user_perms_summary',
871 902 route_name='edit_user_perms_summary', request_method='GET',
872 903 renderer='rhodecode:templates/admin/users/user_edit.mako')
873 904
874 905 config.add_route(
875 906 name='edit_user_perms_summary_json',
876 907 pattern=r'/users/{user_id:\d+}/edit/permissions_summary/json',
877 908 user_route=True)
878 909 config.add_view(
879 910 UsersView,
880 911 attr='user_perms_summary_json',
881 912 route_name='edit_user_perms_summary_json', request_method='GET',
882 913 renderer='json_ext')
883 914
884 915 # user user groups management
885 916 config.add_route(
886 917 name='edit_user_groups_management',
887 918 pattern=r'/users/{user_id:\d+}/edit/groups_management',
888 919 user_route=True)
889 920 config.add_view(
890 921 UsersView,
891 922 attr='groups_management',
892 923 route_name='edit_user_groups_management', request_method='GET',
893 924 renderer='rhodecode:templates/admin/users/user_edit.mako')
894 925
895 926 config.add_route(
896 927 name='edit_user_groups_management_updates',
897 928 pattern=r'/users/{user_id:\d+}/edit/edit_user_groups_management/updates',
898 929 user_route=True)
899 930 config.add_view(
900 931 UsersView,
901 932 attr='groups_management_updates',
902 933 route_name='edit_user_groups_management_updates', request_method='POST')
903 934
904 935 # user audit logs
905 936 config.add_route(
906 937 name='edit_user_audit_logs',
907 938 pattern=r'/users/{user_id:\d+}/edit/audit', user_route=True)
908 939 config.add_view(
909 940 UsersView,
910 941 attr='user_audit_logs',
911 942 route_name='edit_user_audit_logs', request_method='GET',
912 943 renderer='rhodecode:templates/admin/users/user_edit.mako')
913 944
914 945 config.add_route(
915 946 name='edit_user_audit_logs_download',
916 947 pattern=r'/users/{user_id:\d+}/edit/audit/download', user_route=True)
917 948 config.add_view(
918 949 UsersView,
919 950 attr='user_audit_logs_download',
920 951 route_name='edit_user_audit_logs_download', request_method='GET',
921 952 renderer='string')
922 953
923 954 # user caches
924 955 config.add_route(
925 956 name='edit_user_caches',
926 957 pattern=r'/users/{user_id:\d+}/edit/caches',
927 958 user_route=True)
928 959 config.add_view(
929 960 UsersView,
930 961 attr='user_caches',
931 962 route_name='edit_user_caches', request_method='GET',
932 963 renderer='rhodecode:templates/admin/users/user_edit.mako')
933 964
934 965 config.add_route(
935 966 name='edit_user_caches_update',
936 967 pattern=r'/users/{user_id:\d+}/edit/caches/update',
937 968 user_route=True)
938 969 config.add_view(
939 970 UsersView,
940 971 attr='user_caches_update',
941 972 route_name='edit_user_caches_update', request_method='POST')
942 973
943 974 # user-groups admin
944 975 config.add_route(
945 976 name='user_groups',
946 977 pattern='/user_groups')
947 978 config.add_view(
948 979 AdminUserGroupsView,
949 980 attr='user_groups_list',
950 981 route_name='user_groups', request_method='GET',
951 982 renderer='rhodecode:templates/admin/user_groups/user_groups.mako')
952 983
953 984 config.add_route(
954 985 name='user_groups_data',
955 986 pattern='/user_groups_data')
956 987 config.add_view(
957 988 AdminUserGroupsView,
958 989 attr='user_groups_list_data',
959 990 route_name='user_groups_data', request_method='GET',
960 991 renderer='json_ext', xhr=True)
961 992
962 993 config.add_route(
963 994 name='user_groups_new',
964 995 pattern='/user_groups/new')
965 996 config.add_view(
966 997 AdminUserGroupsView,
967 998 attr='user_groups_new',
968 999 route_name='user_groups_new', request_method='GET',
969 1000 renderer='rhodecode:templates/admin/user_groups/user_group_add.mako')
970 1001
971 1002 config.add_route(
972 1003 name='user_groups_create',
973 1004 pattern='/user_groups/create')
974 1005 config.add_view(
975 1006 AdminUserGroupsView,
976 1007 attr='user_groups_create',
977 1008 route_name='user_groups_create', request_method='POST',
978 1009 renderer='rhodecode:templates/admin/user_groups/user_group_add.mako')
979 1010
980 1011 # repos admin
981 1012 config.add_route(
982 1013 name='repos',
983 1014 pattern='/repos')
984 1015 config.add_view(
985 1016 AdminReposView,
986 1017 attr='repository_list',
987 1018 route_name='repos', request_method='GET',
988 1019 renderer='rhodecode:templates/admin/repos/repos.mako')
989 1020
990 1021 config.add_route(
991 1022 name='repos_data',
992 1023 pattern='/repos_data')
993 1024 config.add_view(
994 1025 AdminReposView,
995 1026 attr='repository_list_data',
996 1027 route_name='repos_data', request_method='GET',
997 1028 renderer='json_ext', xhr=True)
998 1029
999 1030 config.add_route(
1000 1031 name='repo_new',
1001 1032 pattern='/repos/new')
1002 1033 config.add_view(
1003 1034 AdminReposView,
1004 1035 attr='repository_new',
1005 1036 route_name='repo_new', request_method='GET',
1006 1037 renderer='rhodecode:templates/admin/repos/repo_add.mako')
1007 1038
1008 1039 config.add_route(
1009 1040 name='repo_create',
1010 1041 pattern='/repos/create')
1011 1042 config.add_view(
1012 1043 AdminReposView,
1013 1044 attr='repository_create',
1014 1045 route_name='repo_create', request_method='POST',
1015 1046 renderer='rhodecode:templates/admin/repos/repos.mako')
1016 1047
1017 1048 # repo groups admin
1018 1049 config.add_route(
1019 1050 name='repo_groups',
1020 1051 pattern='/repo_groups')
1021 1052 config.add_view(
1022 1053 AdminRepoGroupsView,
1023 1054 attr='repo_group_list',
1024 1055 route_name='repo_groups', request_method='GET',
1025 1056 renderer='rhodecode:templates/admin/repo_groups/repo_groups.mako')
1026 1057
1027 1058 config.add_route(
1028 1059 name='repo_groups_data',
1029 1060 pattern='/repo_groups_data')
1030 1061 config.add_view(
1031 1062 AdminRepoGroupsView,
1032 1063 attr='repo_group_list_data',
1033 1064 route_name='repo_groups_data', request_method='GET',
1034 1065 renderer='json_ext', xhr=True)
1035 1066
1036 1067 config.add_route(
1037 1068 name='repo_group_new',
1038 1069 pattern='/repo_group/new')
1039 1070 config.add_view(
1040 1071 AdminRepoGroupsView,
1041 1072 attr='repo_group_new',
1042 1073 route_name='repo_group_new', request_method='GET',
1043 1074 renderer='rhodecode:templates/admin/repo_groups/repo_group_add.mako')
1044 1075
1045 1076 config.add_route(
1046 1077 name='repo_group_create',
1047 1078 pattern='/repo_group/create')
1048 1079 config.add_view(
1049 1080 AdminRepoGroupsView,
1050 1081 attr='repo_group_create',
1051 1082 route_name='repo_group_create', request_method='POST',
1052 1083 renderer='rhodecode:templates/admin/repo_groups/repo_group_add.mako')
1053 1084
1054 1085
1055 1086 def includeme(config):
1056 1087 # Create admin navigation registry and add it to the pyramid registry.
1057 1088 nav_includeme(config)
1058 1089
1059 1090 # main admin routes
1060 1091 config.add_route(
1061 1092 name='admin_home', pattern=ADMIN_PREFIX)
1062 1093 config.add_view(
1063 1094 AdminMainView,
1064 1095 attr='admin_main',
1065 1096 route_name='admin_home', request_method='GET',
1066 1097 renderer='rhodecode:templates/admin/main.mako')
1067 1098
1068 1099 # pr global redirect
1069 1100 config.add_route(
1070 1101 name='pull_requests_global_0', # backward compat
1071 1102 pattern=ADMIN_PREFIX + r'/pull_requests/{pull_request_id:\d+}')
1072 1103 config.add_view(
1073 1104 AdminMainView,
1074 1105 attr='pull_requests',
1075 1106 route_name='pull_requests_global_0', request_method='GET')
1076 1107
1077 1108 config.add_route(
1078 1109 name='pull_requests_global_1', # backward compat
1079 1110 pattern=ADMIN_PREFIX + r'/pull-requests/{pull_request_id:\d+}')
1080 1111 config.add_view(
1081 1112 AdminMainView,
1082 1113 attr='pull_requests',
1083 1114 route_name='pull_requests_global_1', request_method='GET')
1084 1115
1085 1116 config.add_route(
1086 1117 name='pull_requests_global',
1087 1118 pattern=ADMIN_PREFIX + r'/pull-request/{pull_request_id:\d+}')
1088 1119 config.add_view(
1089 1120 AdminMainView,
1090 1121 attr='pull_requests',
1091 1122 route_name='pull_requests_global', request_method='GET')
1092 1123
1093 1124 config.include(admin_routes, route_prefix=ADMIN_PREFIX)
@@ -1,708 +1,715 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 import logging
21 21 import collections
22 22
23 23 import datetime
24 24 import formencode
25 25 import formencode.htmlfill
26 26
27 27 import rhodecode
28 28
29 29 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
30 30 from pyramid.renderers import render
31 31 from pyramid.response import Response
32 32
33 33 from rhodecode.apps._base import BaseAppView
34 34 from rhodecode.apps._base.navigation import navigation_list
35 35 from rhodecode.apps.svn_support import config_keys
36 36 from rhodecode.lib import helpers as h
37 37 from rhodecode.lib.auth import (
38 38 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
39 39 from rhodecode.lib.celerylib import tasks, run_task
40 40 from rhodecode.lib.str_utils import safe_str
41 41 from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_repo_store_path
42 42 from rhodecode.lib.utils2 import str2bool, AttributeDict
43 43 from rhodecode.lib.index import searcher_from_config
44 44
45 45 from rhodecode.model.db import RhodeCodeUi, Repository
46 46 from rhodecode.model.forms import (ApplicationSettingsForm,
47 47 ApplicationUiSettingsForm, ApplicationVisualisationForm,
48 48 LabsSettingsForm, IssueTrackerPatternsForm)
49 49 from rhodecode.model.permission import PermissionModel
50 50 from rhodecode.model.repo_group import RepoGroupModel
51 51
52 52 from rhodecode.model.scm import ScmModel
53 53 from rhodecode.model.notification import EmailNotificationModel
54 54 from rhodecode.model.meta import Session
55 55 from rhodecode.model.settings import (
56 56 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
57 57 SettingsModel)
58 58
59 59
60 60 log = logging.getLogger(__name__)
61 61
62 62
63 63 class AdminSettingsView(BaseAppView):
64 64
65 65 def load_default_context(self):
66 66 c = self._get_local_tmpl_context()
67 67 c.labs_active = str2bool(
68 68 rhodecode.CONFIG.get('labs_settings_active', 'true'))
69 69 c.navlist = navigation_list(self.request)
70 70 return c
71 71
72 72 @classmethod
73 73 def _get_ui_settings(cls):
74 74 ret = RhodeCodeUi.query().all()
75 75
76 76 if not ret:
77 77 raise Exception('Could not get application ui settings !')
78 settings = {}
78 settings = {
79 # legacy param that needs to be kept
80 'web_push_ssl': False
81 }
79 82 for each in ret:
80 83 k = each.ui_key
81 84 v = each.ui_value
85 # skip some options if they are defined
86 if k in ['push_ssl']:
87 continue
88
82 89 if k == '/':
83 90 k = 'root_path'
84 91
85 if k in ['push_ssl', 'publish', 'enabled']:
92 if k in ['publish', 'enabled']:
86 93 v = str2bool(v)
87 94
88 95 if k.find('.') != -1:
89 96 k = k.replace('.', '_')
90 97
91 98 if each.ui_section in ['hooks', 'extensions']:
92 99 v = each.ui_active
93 100
94 101 settings[each.ui_section + '_' + k] = v
102
95 103 return settings
96 104
97 105 @classmethod
98 106 def _form_defaults(cls):
99 107 defaults = SettingsModel().get_all_settings()
100 108 defaults.update(cls._get_ui_settings())
101 109
102 110 defaults.update({
103 111 'new_svn_branch': '',
104 112 'new_svn_tag': '',
105 113 })
106 114 return defaults
107 115
108 116 @LoginRequired()
109 117 @HasPermissionAllDecorator('hg.admin')
110 118 def settings_vcs(self):
111 119 c = self.load_default_context()
112 120 c.active = 'vcs'
113 121 model = VcsSettingsModel()
114 122 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
115 123 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
116 124 c.svn_generate_config = rhodecode.ConfigGet().get_bool(config_keys.generate_config)
117 125 c.svn_config_path = rhodecode.ConfigGet().get_str(config_keys.config_file_path)
118 126 defaults = self._form_defaults()
119 127
120 128 model.create_largeobjects_dirs_if_needed(defaults['paths_root_path'])
121 129
122 130 data = render('rhodecode:templates/admin/settings/settings.mako',
123 131 self._get_template_context(c), self.request)
124 132 html = formencode.htmlfill.render(
125 133 data,
126 134 defaults=defaults,
127 135 encoding="UTF-8",
128 136 force_defaults=False
129 137 )
130 138 return Response(html)
131 139
132 140 @LoginRequired()
133 141 @HasPermissionAllDecorator('hg.admin')
134 142 @CSRFRequired()
135 143 def settings_vcs_update(self):
136 144 _ = self.request.translate
137 145 c = self.load_default_context()
138 146 c.active = 'vcs'
139 147
140 148 model = VcsSettingsModel()
141 149 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
142 150 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
143 151
144 152 c.svn_generate_config = rhodecode.ConfigGet().get_bool(config_keys.generate_config)
145 153 c.svn_config_path = rhodecode.ConfigGet().get_str(config_keys.config_file_path)
146 154 application_form = ApplicationUiSettingsForm(self.request.translate)()
147 155
148 156 try:
149 157 form_result = application_form.to_python(dict(self.request.POST))
150 158 except formencode.Invalid as errors:
151 159 h.flash(
152 160 _("Some form inputs contain invalid data."),
153 161 category='error')
154 162 data = render('rhodecode:templates/admin/settings/settings.mako',
155 163 self._get_template_context(c), self.request)
156 164 html = formencode.htmlfill.render(
157 165 data,
158 166 defaults=errors.value,
159 167 errors=errors.unpack_errors() or {},
160 168 prefix_error=False,
161 169 encoding="UTF-8",
162 170 force_defaults=False
163 171 )
164 172 return Response(html)
165 173
166 174 try:
167 model.update_global_ssl_setting(form_result['web_push_ssl'])
168 175 model.update_global_hook_settings(form_result)
169 176
170 177 model.create_or_update_global_svn_settings(form_result)
171 178 model.create_or_update_global_hg_settings(form_result)
172 179 model.create_or_update_global_git_settings(form_result)
173 180 model.create_or_update_global_pr_settings(form_result)
174 181 except Exception:
175 182 log.exception("Exception while updating settings")
176 183 h.flash(_('Error occurred during updating '
177 184 'application settings'), category='error')
178 185 else:
179 186 Session().commit()
180 187 h.flash(_('Updated VCS settings'), category='success')
181 188 raise HTTPFound(h.route_path('admin_settings_vcs'))
182 189
183 190 data = render('rhodecode:templates/admin/settings/settings.mako',
184 191 self._get_template_context(c), self.request)
185 192 html = formencode.htmlfill.render(
186 193 data,
187 194 defaults=self._form_defaults(),
188 195 encoding="UTF-8",
189 196 force_defaults=False
190 197 )
191 198 return Response(html)
192 199
193 200 @LoginRequired()
194 201 @HasPermissionAllDecorator('hg.admin')
195 202 @CSRFRequired()
196 203 def settings_vcs_delete_svn_pattern(self):
197 204 delete_pattern_id = self.request.POST.get('delete_svn_pattern')
198 205 model = VcsSettingsModel()
199 206 try:
200 207 model.delete_global_svn_pattern(delete_pattern_id)
201 208 except SettingNotFound:
202 209 log.exception(
203 210 'Failed to delete svn_pattern with id %s', delete_pattern_id)
204 211 raise HTTPNotFound()
205 212
206 213 Session().commit()
207 214 return True
208 215
209 216 @LoginRequired()
210 217 @HasPermissionAllDecorator('hg.admin')
211 218 def settings_mapping(self):
212 219 c = self.load_default_context()
213 220 c.active = 'mapping'
214 221 c.storage_path = get_rhodecode_repo_store_path()
215 222 data = render('rhodecode:templates/admin/settings/settings.mako',
216 223 self._get_template_context(c), self.request)
217 224 html = formencode.htmlfill.render(
218 225 data,
219 226 defaults=self._form_defaults(),
220 227 encoding="UTF-8",
221 228 force_defaults=False
222 229 )
223 230 return Response(html)
224 231
225 232 @LoginRequired()
226 233 @HasPermissionAllDecorator('hg.admin')
227 234 @CSRFRequired()
228 235 def settings_mapping_update(self):
229 236 _ = self.request.translate
230 237 c = self.load_default_context()
231 238 c.active = 'mapping'
232 239 rm_obsolete = self.request.POST.get('destroy', False)
233 240 invalidate_cache = self.request.POST.get('invalidate', False)
234 241 log.debug('rescanning repo location with destroy obsolete=%s', rm_obsolete)
235 242
236 243 if invalidate_cache:
237 244 log.debug('invalidating all repositories cache')
238 245 for repo in Repository.get_all():
239 246 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
240 247
241 248 filesystem_repos = ScmModel().repo_scan()
242 249 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete, force_hooks_rebuild=True)
243 250 PermissionModel().trigger_permission_flush()
244 251
245 252 def _repr(rm_repo):
246 253 return ', '.join(map(safe_str, rm_repo)) or '-'
247 254
248 255 h.flash(_('Repositories successfully '
249 256 'rescanned added: %s ; removed: %s') %
250 257 (_repr(added), _repr(removed)),
251 258 category='success')
252 259 raise HTTPFound(h.route_path('admin_settings_mapping'))
253 260
254 261 @LoginRequired()
255 262 @HasPermissionAllDecorator('hg.admin')
256 263 def settings_global(self):
257 264 c = self.load_default_context()
258 265 c.active = 'global'
259 266 c.personal_repo_group_default_pattern = RepoGroupModel()\
260 267 .get_personal_group_name_pattern()
261 268
262 269 data = render('rhodecode:templates/admin/settings/settings.mako',
263 270 self._get_template_context(c), self.request)
264 271 html = formencode.htmlfill.render(
265 272 data,
266 273 defaults=self._form_defaults(),
267 274 encoding="UTF-8",
268 275 force_defaults=False
269 276 )
270 277 return Response(html)
271 278
272 279 @LoginRequired()
273 280 @HasPermissionAllDecorator('hg.admin')
274 281 @CSRFRequired()
275 282 def settings_global_update(self):
276 283 _ = self.request.translate
277 284 c = self.load_default_context()
278 285 c.active = 'global'
279 286 c.personal_repo_group_default_pattern = RepoGroupModel()\
280 287 .get_personal_group_name_pattern()
281 288 application_form = ApplicationSettingsForm(self.request.translate)()
282 289 try:
283 290 form_result = application_form.to_python(dict(self.request.POST))
284 291 except formencode.Invalid as errors:
285 292 h.flash(
286 293 _("Some form inputs contain invalid data."),
287 294 category='error')
288 295 data = render('rhodecode:templates/admin/settings/settings.mako',
289 296 self._get_template_context(c), self.request)
290 297 html = formencode.htmlfill.render(
291 298 data,
292 299 defaults=errors.value,
293 300 errors=errors.unpack_errors() or {},
294 301 prefix_error=False,
295 302 encoding="UTF-8",
296 303 force_defaults=False
297 304 )
298 305 return Response(html)
299 306
300 307 settings = [
301 308 ('title', 'rhodecode_title', 'unicode'),
302 309 ('realm', 'rhodecode_realm', 'unicode'),
303 310 ('pre_code', 'rhodecode_pre_code', 'unicode'),
304 311 ('post_code', 'rhodecode_post_code', 'unicode'),
305 312 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
306 313 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
307 314 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
308 315 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
309 316 ]
310 317
311 318 try:
312 319 for setting, form_key, type_ in settings:
313 320 sett = SettingsModel().create_or_update_setting(
314 321 setting, form_result[form_key], type_)
315 322 Session().add(sett)
316 323
317 324 Session().commit()
318 325 SettingsModel().invalidate_settings_cache()
319 326 h.flash(_('Updated application settings'), category='success')
320 327 except Exception:
321 328 log.exception("Exception while updating application settings")
322 329 h.flash(
323 330 _('Error occurred during updating application settings'),
324 331 category='error')
325 332
326 333 raise HTTPFound(h.route_path('admin_settings_global'))
327 334
328 335 @LoginRequired()
329 336 @HasPermissionAllDecorator('hg.admin')
330 337 def settings_visual(self):
331 338 c = self.load_default_context()
332 339 c.active = 'visual'
333 340
334 341 data = render('rhodecode:templates/admin/settings/settings.mako',
335 342 self._get_template_context(c), self.request)
336 343 html = formencode.htmlfill.render(
337 344 data,
338 345 defaults=self._form_defaults(),
339 346 encoding="UTF-8",
340 347 force_defaults=False
341 348 )
342 349 return Response(html)
343 350
344 351 @LoginRequired()
345 352 @HasPermissionAllDecorator('hg.admin')
346 353 @CSRFRequired()
347 354 def settings_visual_update(self):
348 355 _ = self.request.translate
349 356 c = self.load_default_context()
350 357 c.active = 'visual'
351 358 application_form = ApplicationVisualisationForm(self.request.translate)()
352 359 try:
353 360 form_result = application_form.to_python(dict(self.request.POST))
354 361 except formencode.Invalid as errors:
355 362 h.flash(
356 363 _("Some form inputs contain invalid data."),
357 364 category='error')
358 365 data = render('rhodecode:templates/admin/settings/settings.mako',
359 366 self._get_template_context(c), self.request)
360 367 html = formencode.htmlfill.render(
361 368 data,
362 369 defaults=errors.value,
363 370 errors=errors.unpack_errors() or {},
364 371 prefix_error=False,
365 372 encoding="UTF-8",
366 373 force_defaults=False
367 374 )
368 375 return Response(html)
369 376
370 377 try:
371 378 settings = [
372 379 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
373 380 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
374 381 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
375 382 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
376 383 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
377 384 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
378 385 ('show_version', 'rhodecode_show_version', 'bool'),
379 386 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
380 387 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
381 388 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
382 389 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
383 390 ('clone_uri_id_tmpl', 'rhodecode_clone_uri_id_tmpl', 'unicode'),
384 391 ('clone_uri_ssh_tmpl', 'rhodecode_clone_uri_ssh_tmpl', 'unicode'),
385 392 ('support_url', 'rhodecode_support_url', 'unicode'),
386 393 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
387 394 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
388 395 ]
389 396 for setting, form_key, type_ in settings:
390 397 sett = SettingsModel().create_or_update_setting(
391 398 setting, form_result[form_key], type_)
392 399 Session().add(sett)
393 400
394 401 Session().commit()
395 402 SettingsModel().invalidate_settings_cache()
396 403 h.flash(_('Updated visualisation settings'), category='success')
397 404 except Exception:
398 405 log.exception("Exception updating visualization settings")
399 406 h.flash(_('Error occurred during updating '
400 407 'visualisation settings'),
401 408 category='error')
402 409
403 410 raise HTTPFound(h.route_path('admin_settings_visual'))
404 411
405 412 @LoginRequired()
406 413 @HasPermissionAllDecorator('hg.admin')
407 414 def settings_issuetracker(self):
408 415 c = self.load_default_context()
409 416 c.active = 'issuetracker'
410 417 defaults = c.rc_config
411 418
412 419 entry_key = 'rhodecode_issuetracker_pat_'
413 420
414 421 c.issuetracker_entries = {}
415 422 for k, v in defaults.items():
416 423 if k.startswith(entry_key):
417 424 uid = k[len(entry_key):]
418 425 c.issuetracker_entries[uid] = None
419 426
420 427 for uid in c.issuetracker_entries:
421 428 c.issuetracker_entries[uid] = AttributeDict({
422 429 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
423 430 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
424 431 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
425 432 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
426 433 })
427 434
428 435 return self._get_template_context(c)
429 436
430 437 @LoginRequired()
431 438 @HasPermissionAllDecorator('hg.admin')
432 439 @CSRFRequired()
433 440 def settings_issuetracker_test(self):
434 441 error_container = []
435 442
436 443 urlified_commit = h.urlify_commit_message(
437 444 self.request.POST.get('test_text', ''),
438 445 'repo_group/test_repo1', error_container=error_container)
439 446 if error_container:
440 447 def converter(inp):
441 448 return h.html_escape(inp)
442 449
443 450 return 'ERRORS: ' + '\n'.join(map(converter, error_container))
444 451
445 452 return urlified_commit
446 453
447 454 @LoginRequired()
448 455 @HasPermissionAllDecorator('hg.admin')
449 456 @CSRFRequired()
450 457 def settings_issuetracker_update(self):
451 458 _ = self.request.translate
452 459 self.load_default_context()
453 460 settings_model = IssueTrackerSettingsModel()
454 461
455 462 try:
456 463 form = IssueTrackerPatternsForm(self.request.translate)()
457 464 data = form.to_python(self.request.POST)
458 465 except formencode.Invalid as errors:
459 466 log.exception('Failed to add new pattern')
460 467 error = errors
461 468 h.flash(_(f'Invalid issue tracker pattern: {error}'),
462 469 category='error')
463 470 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
464 471
465 472 if data:
466 473 for uid in data.get('delete_patterns', []):
467 474 settings_model.delete_entries(uid)
468 475
469 476 for pattern in data.get('patterns', []):
470 477 for setting, value, type_ in pattern:
471 478 sett = settings_model.create_or_update_setting(
472 479 setting, value, type_)
473 480 Session().add(sett)
474 481
475 482 Session().commit()
476 483
477 484 SettingsModel().invalidate_settings_cache()
478 485 h.flash(_('Updated issue tracker entries'), category='success')
479 486 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
480 487
481 488 @LoginRequired()
482 489 @HasPermissionAllDecorator('hg.admin')
483 490 @CSRFRequired()
484 491 def settings_issuetracker_delete(self):
485 492 _ = self.request.translate
486 493 self.load_default_context()
487 494 uid = self.request.POST.get('uid')
488 495 try:
489 496 IssueTrackerSettingsModel().delete_entries(uid)
490 497 except Exception:
491 498 log.exception('Failed to delete issue tracker setting %s', uid)
492 499 raise HTTPNotFound()
493 500
494 501 SettingsModel().invalidate_settings_cache()
495 502 h.flash(_('Removed issue tracker entry.'), category='success')
496 503
497 504 return {'deleted': uid}
498 505
499 506 @LoginRequired()
500 507 @HasPermissionAllDecorator('hg.admin')
501 508 def settings_email(self):
502 509 c = self.load_default_context()
503 510 c.active = 'email'
504 511 c.rhodecode_ini = rhodecode.CONFIG
505 512
506 513 data = render('rhodecode:templates/admin/settings/settings.mako',
507 514 self._get_template_context(c), self.request)
508 515 html = formencode.htmlfill.render(
509 516 data,
510 517 defaults=self._form_defaults(),
511 518 encoding="UTF-8",
512 519 force_defaults=False
513 520 )
514 521 return Response(html)
515 522
516 523 @LoginRequired()
517 524 @HasPermissionAllDecorator('hg.admin')
518 525 @CSRFRequired()
519 526 def settings_email_update(self):
520 527 _ = self.request.translate
521 528 c = self.load_default_context()
522 529 c.active = 'email'
523 530
524 531 test_email = self.request.POST.get('test_email')
525 532
526 533 if not test_email:
527 534 h.flash(_('Please enter email address'), category='error')
528 535 raise HTTPFound(h.route_path('admin_settings_email'))
529 536
530 537 email_kwargs = {
531 538 'date': datetime.datetime.now(),
532 539 'user': self._rhodecode_db_user
533 540 }
534 541
535 542 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
536 543 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
537 544
538 545 recipients = [test_email] if test_email else None
539 546
540 547 run_task(tasks.send_email, recipients, subject,
541 548 email_body_plaintext, email_body)
542 549
543 550 h.flash(_('Send email task created'), category='success')
544 551 raise HTTPFound(h.route_path('admin_settings_email'))
545 552
546 553 @LoginRequired()
547 554 @HasPermissionAllDecorator('hg.admin')
548 555 def settings_hooks(self):
549 556 c = self.load_default_context()
550 557 c.active = 'hooks'
551 558
552 559 model = SettingsModel()
553 560 c.hooks = model.get_builtin_hooks()
554 561 c.custom_hooks = model.get_custom_hooks()
555 562
556 563 data = render('rhodecode:templates/admin/settings/settings.mako',
557 564 self._get_template_context(c), self.request)
558 565 html = formencode.htmlfill.render(
559 566 data,
560 567 defaults=self._form_defaults(),
561 568 encoding="UTF-8",
562 569 force_defaults=False
563 570 )
564 571 return Response(html)
565 572
566 573 @LoginRequired()
567 574 @HasPermissionAllDecorator('hg.admin')
568 575 @CSRFRequired()
569 576 def settings_hooks_update(self):
570 577 _ = self.request.translate
571 578 c = self.load_default_context()
572 579 c.active = 'hooks'
573 580 if c.visual.allow_custom_hooks_settings:
574 581 ui_key = self.request.POST.get('new_hook_ui_key')
575 582 ui_value = self.request.POST.get('new_hook_ui_value')
576 583
577 584 hook_id = self.request.POST.get('hook_id')
578 585 new_hook = False
579 586
580 587 model = SettingsModel()
581 588 try:
582 589 if ui_value and ui_key:
583 590 model.create_or_update_hook(ui_key, ui_value)
584 591 h.flash(_('Added new hook'), category='success')
585 592 new_hook = True
586 593 elif hook_id:
587 594 RhodeCodeUi.delete(hook_id)
588 595 Session().commit()
589 596
590 597 # check for edits
591 598 update = False
592 599 _d = self.request.POST.dict_of_lists()
593 600 for k, v in zip(_d.get('hook_ui_key', []),
594 601 _d.get('hook_ui_value_new', [])):
595 602 model.create_or_update_hook(k, v)
596 603 update = True
597 604
598 605 if update and not new_hook:
599 606 h.flash(_('Updated hooks'), category='success')
600 607 Session().commit()
601 608 except Exception:
602 609 log.exception("Exception during hook creation")
603 610 h.flash(_('Error occurred during hook creation'),
604 611 category='error')
605 612
606 613 raise HTTPFound(h.route_path('admin_settings_hooks'))
607 614
608 615 @LoginRequired()
609 616 @HasPermissionAllDecorator('hg.admin')
610 617 def settings_search(self):
611 618 c = self.load_default_context()
612 619 c.active = 'search'
613 620
614 621 c.searcher = searcher_from_config(self.request.registry.settings)
615 622 c.statistics = c.searcher.statistics(self.request.translate)
616 623
617 624 return self._get_template_context(c)
618 625
619 626 @LoginRequired()
620 627 @HasPermissionAllDecorator('hg.admin')
621 628 def settings_labs(self):
622 629 c = self.load_default_context()
623 630 if not c.labs_active:
624 631 raise HTTPFound(h.route_path('admin_settings'))
625 632
626 633 c.active = 'labs'
627 634 c.lab_settings = _LAB_SETTINGS
628 635
629 636 data = render('rhodecode:templates/admin/settings/settings.mako',
630 637 self._get_template_context(c), self.request)
631 638 html = formencode.htmlfill.render(
632 639 data,
633 640 defaults=self._form_defaults(),
634 641 encoding="UTF-8",
635 642 force_defaults=False
636 643 )
637 644 return Response(html)
638 645
639 646 @LoginRequired()
640 647 @HasPermissionAllDecorator('hg.admin')
641 648 @CSRFRequired()
642 649 def settings_labs_update(self):
643 650 _ = self.request.translate
644 651 c = self.load_default_context()
645 652 c.active = 'labs'
646 653
647 654 application_form = LabsSettingsForm(self.request.translate)()
648 655 try:
649 656 form_result = application_form.to_python(dict(self.request.POST))
650 657 except formencode.Invalid as errors:
651 658 h.flash(
652 659 _("Some form inputs contain invalid data."),
653 660 category='error')
654 661 data = render('rhodecode:templates/admin/settings/settings.mako',
655 662 self._get_template_context(c), self.request)
656 663 html = formencode.htmlfill.render(
657 664 data,
658 665 defaults=errors.value,
659 666 errors=errors.unpack_errors() or {},
660 667 prefix_error=False,
661 668 encoding="UTF-8",
662 669 force_defaults=False
663 670 )
664 671 return Response(html)
665 672
666 673 try:
667 674 session = Session()
668 675 for setting in _LAB_SETTINGS:
669 676 setting_name = setting.key[len('rhodecode_'):]
670 677 sett = SettingsModel().create_or_update_setting(
671 678 setting_name, form_result[setting.key], setting.type)
672 679 session.add(sett)
673 680
674 681 except Exception:
675 682 log.exception('Exception while updating lab settings')
676 683 h.flash(_('Error occurred during updating labs settings'),
677 684 category='error')
678 685 else:
679 686 Session().commit()
680 687 SettingsModel().invalidate_settings_cache()
681 688 h.flash(_('Updated Labs settings'), category='success')
682 689 raise HTTPFound(h.route_path('admin_settings_labs'))
683 690
684 691 data = render('rhodecode:templates/admin/settings/settings.mako',
685 692 self._get_template_context(c), self.request)
686 693 html = formencode.htmlfill.render(
687 694 data,
688 695 defaults=self._form_defaults(),
689 696 encoding="UTF-8",
690 697 force_defaults=False
691 698 )
692 699 return Response(html)
693 700
694 701
695 702 # :param key: name of the setting including the 'rhodecode_' prefix
696 703 # :param type: the RhodeCodeSetting type to use.
697 704 # :param group: the i18ned group in which we should dispaly this setting
698 705 # :param label: the i18ned label we should display for this setting
699 706 # :param help: the i18ned help we should dispaly for this setting
700 707 LabSetting = collections.namedtuple(
701 708 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
702 709
703 710
704 711 # This list has to be kept in sync with the form
705 712 # rhodecode.model.forms.LabsSettingsForm.
706 713 _LAB_SETTINGS = [
707 714
708 715 ]
@@ -1,243 +1,249 b''
1 1
2 2
3 3 # Copyright (C) 2016-2023 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import urllib.request
23 23 import urllib.error
24 24 import urllib.parse
25 25 import os
26 26
27 27 import rhodecode
28 28 from rhodecode.apps._base import BaseAppView
29 29 from rhodecode.apps._base.navigation import navigation_list
30 30 from rhodecode.lib import helpers as h
31 31 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
32 32 from rhodecode.lib.utils2 import str2bool
33 33 from rhodecode.lib import system_info
34 34 from rhodecode.model.update import UpdateModel
35 35
36 36 log = logging.getLogger(__name__)
37 37
38 38
39 39 class AdminSystemInfoSettingsView(BaseAppView):
40 40 def load_default_context(self):
41 41 c = self._get_local_tmpl_context()
42 42 return c
43 43
44 44 def get_env_data(self):
45 45 black_list = [
46 46 'NIX_LDFLAGS',
47 47 'NIX_CFLAGS_COMPILE',
48 48 'propagatedBuildInputs',
49 49 'propagatedNativeBuildInputs',
50 50 'postInstall',
51 51 'buildInputs',
52 52 'buildPhase',
53 53 'preShellHook',
54 54 'preShellHook',
55 55 'preCheck',
56 56 'preBuild',
57 57 'postShellHook',
58 58 'postFixup',
59 59 'postCheck',
60 60 'nativeBuildInputs',
61 61 'installPhase',
62 62 'installCheckPhase',
63 63 'checkPhase',
64 64 'configurePhase',
65 65 'shellHook'
66 66 ]
67 67 secret_list = [
68 68 'RHODECODE_USER_PASS'
69 69 ]
70 70
71 71 for k, v in sorted(os.environ.items()):
72 72 if k in black_list:
73 73 continue
74 74 if k in secret_list:
75 75 v = '*****'
76 76 yield k, v
77 77
78 78 @LoginRequired()
79 79 @HasPermissionAllDecorator('hg.admin')
80 80 def settings_system_info(self):
81 81 _ = self.request.translate
82 82 c = self.load_default_context()
83 83
84 84 c.active = 'system'
85 85 c.navlist = navigation_list(self.request)
86 86
87 87 # TODO(marcink), figure out how to allow only selected users to do this
88 88 c.allowed_to_snapshot = self._rhodecode_user.admin
89 89
90 90 snapshot = str2bool(self.request.params.get('snapshot'))
91 91
92 92 c.rhodecode_update_url = UpdateModel().get_update_url()
93 93 c.env_data = self.get_env_data()
94 94 server_info = system_info.get_system_info(self.request.environ)
95 95
96 96 for key, val in server_info.items():
97 97 setattr(c, key, val)
98 98
99 99 def val(name, subkey='human_value'):
100 100 return server_info[name][subkey]
101 101
102 102 def state(name):
103 103 return server_info[name]['state']
104 104
105 105 def val2(name):
106 106 val = server_info[name]['human_value']
107 107 state = server_info[name]['state']
108 108 return val, state
109 109
110 110 update_info_msg = _('Note: please make sure this server can '
111 111 'access `${url}` for the update link to work',
112 112 mapping=dict(url=c.rhodecode_update_url))
113 113 version = UpdateModel().get_stored_version()
114 114 is_outdated = UpdateModel().is_outdated(
115 115 rhodecode.__version__, version)
116 116 update_state = {
117 117 'type': 'warning',
118 118 'message': 'New version available: {}'.format(version)
119 119 } \
120 120 if is_outdated else {}
121 121 c.data_items = [
122 122 # update info
123 123 (_('Update info'), h.literal(
124 124 '<span class="link" id="check_for_update" >%s.</span>' % (
125 125 _('Check for updates')) +
126 126 '<br/> <span >%s.</span>' % (update_info_msg)
127 127 ), ''),
128 128
129 129 # RhodeCode specific
130 130 (_('RhodeCode Version'), val('rhodecode_app')['text'], state('rhodecode_app')),
131 131 (_('Latest version'), version, update_state),
132 132 (_('RhodeCode Base URL'), val('rhodecode_config')['config'].get('app.base_url'), state('rhodecode_config')),
133 133 (_('RhodeCode Server IP'), val('server')['server_ip'], state('server')),
134 134 (_('RhodeCode Server ID'), val('server')['server_id'], state('server')),
135 135 (_('RhodeCode Configuration'), val('rhodecode_config')['path'], state('rhodecode_config')),
136 136 (_('RhodeCode Certificate'), val('rhodecode_config')['cert_path'], state('rhodecode_config')),
137 137 (_('Workers'), val('rhodecode_config')['config']['server:main'].get('workers', '?'), state('rhodecode_config')),
138 138 (_('Worker Type'), val('rhodecode_config')['config']['server:main'].get('worker_class', 'sync'), state('rhodecode_config')),
139 139 ('', '', ''), # spacer
140 140
141 141 # Database
142 142 (_('Database'), val('database')['url'], state('database')),
143 143 (_('Database version'), val('database')['version'], state('database')),
144 144 ('', '', ''), # spacer
145 145
146 146 # Platform/Python
147 147 (_('Platform'), val('platform')['name'], state('platform')),
148 148 (_('Platform UUID'), val('platform')['uuid'], state('platform')),
149 149 (_('Lang'), val('locale'), state('locale')),
150 150 (_('Python version'), val('python')['version'], state('python')),
151 151 (_('Python path'), val('python')['executable'], state('python')),
152 152 ('', '', ''), # spacer
153 153
154 154 # Systems stats
155 155 (_('CPU'), val('cpu')['text'], state('cpu')),
156 156 (_('Load'), val('load')['text'], state('load')),
157 157 (_('Memory'), val('memory')['text'], state('memory')),
158 158 (_('Uptime'), val('uptime')['text'], state('uptime')),
159 159 ('', '', ''), # spacer
160 160
161 161 # ulimit
162 162 (_('Ulimit'), val('ulimit')['text'], state('ulimit')),
163 163
164 164 # Repo storage
165 165 (_('Storage location'), val('storage')['path'], state('storage')),
166 166 (_('Storage info'), val('storage')['text'], state('storage')),
167 167 (_('Storage inodes'), val('storage_inodes')['text'], state('storage_inodes')),
168 168 ('', '', ''), # spacer
169 169
170 170 (_('Gist storage location'), val('storage_gist')['path'], state('storage_gist')),
171 171 (_('Gist storage info'), val('storage_gist')['text'], state('storage_gist')),
172 172 ('', '', ''), # spacer
173 173
174 (_('Archive cache storage type'), val('storage_archive')['type'], state('storage_archive')),
174 (_('Artifacts storage backend'), val('storage_artifacts')['type'], state('storage_artifacts')),
175 (_('Artifacts storage location'), val('storage_artifacts')['path'], state('storage_artifacts')),
176 (_('Artifacts info'), val('storage_artifacts')['text'], state('storage_artifacts')),
177 ('', '', ''), # spacer
178
179 (_('Archive cache storage backend'), val('storage_archive')['type'], state('storage_archive')),
175 180 (_('Archive cache storage location'), val('storage_archive')['path'], state('storage_archive')),
176 181 (_('Archive cache info'), val('storage_archive')['text'], state('storage_archive')),
177 182 ('', '', ''), # spacer
178 183
184
179 185 (_('Temp storage location'), val('storage_temp')['path'], state('storage_temp')),
180 186 (_('Temp storage info'), val('storage_temp')['text'], state('storage_temp')),
181 187 ('', '', ''), # spacer
182 188
183 189 (_('Search info'), val('search')['text'], state('search')),
184 190 (_('Search location'), val('search')['location'], state('search')),
185 191 ('', '', ''), # spacer
186 192
187 193 # VCS specific
188 194 (_('VCS Backends'), val('vcs_backends'), state('vcs_backends')),
189 195 (_('VCS Server'), val('vcs_server')['text'], state('vcs_server')),
190 196 (_('GIT'), val('git'), state('git')),
191 197 (_('HG'), val('hg'), state('hg')),
192 198 (_('SVN'), val('svn'), state('svn')),
193 199
194 200 ]
195 201
196 202 c.vcsserver_data_items = [
197 203 (k, v) for k, v in (val('vcs_server_config') or {}).items()
198 204 ]
199 205
200 206 if snapshot:
201 207 if c.allowed_to_snapshot:
202 208 c.data_items.pop(0) # remove server info
203 209 self.request.override_renderer = 'admin/settings/settings_system_snapshot.mako'
204 210 else:
205 211 h.flash('You are not allowed to do this', category='warning')
206 212 return self._get_template_context(c)
207 213
208 214 @LoginRequired()
209 215 @HasPermissionAllDecorator('hg.admin')
210 216 def settings_system_info_check_update(self):
211 217 _ = self.request.translate
212 218 c = self.load_default_context()
213 219
214 220 update_url = UpdateModel().get_update_url()
215 221
216 222 def _err(s):
217 223 return f'<div style="color:#ff8888; padding:4px 0px">{s}</div>'
218 224
219 225 try:
220 226 data = UpdateModel().get_update_data(update_url)
221 227 except urllib.error.URLError as e:
222 228 log.exception("Exception contacting upgrade server")
223 229 self.request.override_renderer = 'string'
224 230 return _err('Failed to contact upgrade server: %r' % e)
225 231 except ValueError as e:
226 232 log.exception("Bad data sent from update server")
227 233 self.request.override_renderer = 'string'
228 234 return _err('Bad data sent from update server')
229 235
230 236 latest = data['versions'][0]
231 237
232 238 c.update_url = update_url
233 239 c.latest_data = latest
234 240 c.latest_ver = (latest['version'] or '').strip()
235 241 c.cur_ver = self.request.GET.get('ver') or rhodecode.__version__
236 242 c.should_upgrade = False
237 243
238 244 is_outdated = UpdateModel().is_outdated(c.cur_ver, c.latest_ver)
239 245 if is_outdated:
240 246 c.should_upgrade = True
241 247 c.important_notices = latest['general']
242 248 UpdateModel().store_version(latest['version'])
243 249 return self._get_template_context(c)
@@ -1,66 +1,97 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18 import os
19 from rhodecode.apps.file_store import config_keys
19
20
20 21 from rhodecode.config.settings_maker import SettingsMaker
21 22
22 23
23 24 def _sanitize_settings_and_apply_defaults(settings):
24 25 """
25 26 Set defaults, convert to python types and validate settings.
26 27 """
28 from rhodecode.apps.file_store import config_keys
29
30 # translate "legacy" params into new config
31 settings.pop(config_keys.deprecated_enabled, True)
32 if config_keys.deprecated_backend in settings:
33 # if legacy backend key is detected we use "legacy" backward compat setting
34 settings.pop(config_keys.deprecated_backend)
35 settings[config_keys.backend_type] = config_keys.backend_legacy_filesystem
36
37 if config_keys.deprecated_store_path in settings:
38 store_path = settings.pop(config_keys.deprecated_store_path)
39 settings[config_keys.legacy_filesystem_storage_path] = store_path
40
27 41 settings_maker = SettingsMaker(settings)
28 42
29 settings_maker.make_setting(config_keys.enabled, True, parser='bool')
30 settings_maker.make_setting(config_keys.backend, 'local')
43 default_cache_dir = settings['cache_dir']
44 default_store_dir = os.path.join(default_cache_dir, 'artifacts_filestore')
45
46 # set default backend
47 settings_maker.make_setting(config_keys.backend_type, config_keys.backend_legacy_filesystem)
48
49 # legacy filesystem defaults
50 settings_maker.make_setting(config_keys.legacy_filesystem_storage_path, default_store_dir, default_when_empty=True, )
31 51
32 default_store = os.path.join(os.path.dirname(settings['__file__']), 'upload_store')
33 settings_maker.make_setting(config_keys.store_path, default_store)
52 # filesystem defaults
53 settings_maker.make_setting(config_keys.filesystem_storage_path, default_store_dir, default_when_empty=True,)
54 settings_maker.make_setting(config_keys.filesystem_shards, 8, parser='int')
55
56 # objectstore defaults
57 settings_maker.make_setting(config_keys.objectstore_url, 'http://s3-minio:9000')
58 settings_maker.make_setting(config_keys.objectstore_bucket, 'rhodecode-artifacts-filestore')
59 settings_maker.make_setting(config_keys.objectstore_bucket_shards, 8, parser='int')
60
61 settings_maker.make_setting(config_keys.objectstore_region, '')
62 settings_maker.make_setting(config_keys.objectstore_key, '')
63 settings_maker.make_setting(config_keys.objectstore_secret, '')
34 64
35 65 settings_maker.env_expand()
36 66
37 67
38 68 def includeme(config):
69
39 70 from rhodecode.apps.file_store.views import FileStoreView
40 71
41 72 settings = config.registry.settings
42 73 _sanitize_settings_and_apply_defaults(settings)
43 74
44 75 config.add_route(
45 76 name='upload_file',
46 77 pattern='/_file_store/upload')
47 78 config.add_view(
48 79 FileStoreView,
49 80 attr='upload_file',
50 81 route_name='upload_file', request_method='POST', renderer='json_ext')
51 82
52 83 config.add_route(
53 84 name='download_file',
54 85 pattern='/_file_store/download/{fid:.*}')
55 86 config.add_view(
56 87 FileStoreView,
57 88 attr='download_file',
58 89 route_name='download_file')
59 90
60 91 config.add_route(
61 92 name='download_file_by_token',
62 93 pattern='/_file_store/token-download/{_auth_token}/{fid:.*}')
63 94 config.add_view(
64 95 FileStoreView,
65 96 attr='download_file_by_token',
66 97 route_name='download_file_by_token')
@@ -1,25 +1,57 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 # Definition of setting keys used to configure this module. Defined here to
21 21 # avoid repetition of keys throughout the module.
22 22
23 enabled = 'file_store.enabled'
24 backend = 'file_store.backend'
25 store_path = 'file_store.storage_path'
23 # OLD and deprecated keys not used anymore
24 deprecated_enabled = 'file_store.enabled'
25 deprecated_backend = 'file_store.backend'
26 deprecated_store_path = 'file_store.storage_path'
27
28
29 backend_type = 'file_store.backend.type'
30
31 backend_legacy_filesystem = 'filesystem_v1'
32 backend_filesystem = 'filesystem_v2'
33 backend_objectstore = 'objectstore'
34
35 backend_types = [
36 backend_legacy_filesystem,
37 backend_filesystem,
38 backend_objectstore,
39 ]
40
41 # filesystem_v1 legacy
42 legacy_filesystem_storage_path = 'file_store.filesystem_v1.storage_path'
43
44
45 # filesystem_v2 new option
46 filesystem_storage_path = 'file_store.filesystem_v2.storage_path'
47 filesystem_shards = 'file_store.filesystem_v2.shards'
48
49 # objectstore
50 objectstore_url = 'file_store.objectstore.url'
51 objectstore_bucket = 'file_store.objectstore.bucket'
52 objectstore_bucket_shards = 'file_store.objectstore.bucket_shards'
53
54 objectstore_region = 'file_store.objectstore.region'
55 objectstore_key = 'file_store.objectstore.key'
56 objectstore_secret = 'file_store.objectstore.secret'
57
@@ -1,18 +1,57 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 import os
20 import random
21 import tempfile
22 import string
23
24 import pytest
25
26 from rhodecode.apps.file_store import utils as store_utils
27
28
29 @pytest.fixture()
30 def file_store_instance(ini_settings):
31 config = ini_settings
32 f_store = store_utils.get_filestore_backend(config=config, always_init=True)
33 return f_store
34
35
36 @pytest.fixture
37 def random_binary_file():
38 # Generate random binary data
39 data = bytearray(random.getrandbits(8) for _ in range(1024 * 512)) # 512 KB of random data
40
41 # Create a temporary file
42 temp_file = tempfile.NamedTemporaryFile(delete=False)
43 filename = temp_file.name
44
45 try:
46 # Write the random binary data to the file
47 temp_file.write(data)
48 temp_file.seek(0) # Rewind the file pointer to the beginning
49 yield filename, temp_file
50 finally:
51 # Close and delete the temporary file after the test
52 temp_file.close()
53 os.remove(filename)
54
55
56 def generate_random_filename(length=10):
57 return ''.join(random.choices(string.ascii_letters + string.digits, k=length)) No newline at end of file
@@ -1,246 +1,253 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18 19 import os
20
19 21 import pytest
20 22
21 23 from rhodecode.lib.ext_json import json
22 24 from rhodecode.model.auth_token import AuthTokenModel
23 25 from rhodecode.model.db import Session, FileStore, Repository, User
24 from rhodecode.apps.file_store import utils, config_keys
26 from rhodecode.apps.file_store import utils as store_utils
27 from rhodecode.apps.file_store import config_keys
25 28
26 29 from rhodecode.tests import TestController
27 30 from rhodecode.tests.routes import route_path
28 31
29 32
30 33 class TestFileStoreViews(TestController):
31 34
35 @pytest.fixture()
36 def create_artifact_factory(self, tmpdir, ini_settings):
37
38 def factory(user_id, content, f_name='example.txt'):
39
40 config = ini_settings
41 config[config_keys.backend_type] = config_keys.backend_legacy_filesystem
42
43 f_store = store_utils.get_filestore_backend(config)
44
45 filesystem_file = os.path.join(str(tmpdir), f_name)
46 with open(filesystem_file, 'wt') as f:
47 f.write(content)
48
49 with open(filesystem_file, 'rb') as f:
50 store_uid, metadata = f_store.store(f_name, f, metadata={'filename': f_name})
51 os.remove(filesystem_file)
52
53 entry = FileStore.create(
54 file_uid=store_uid, filename=metadata["filename"],
55 file_hash=metadata["sha256"], file_size=metadata["size"],
56 file_display_name='file_display_name',
57 file_description='repo artifact `{}`'.format(metadata["filename"]),
58 check_acl=True, user_id=user_id,
59 )
60 Session().add(entry)
61 Session().commit()
62 return entry
63 return factory
64
32 65 @pytest.mark.parametrize("fid, content, exists", [
33 66 ('abcde-0.jpg', "xxxxx", True),
34 67 ('abcde-0.exe', "1234567", True),
35 68 ('abcde-0.jpg', "xxxxx", False),
36 69 ])
37 def test_get_files_from_store(self, fid, content, exists, tmpdir, user_util):
70 def test_get_files_from_store(self, fid, content, exists, tmpdir, user_util, ini_settings):
38 71 user = self.log_user()
39 72 user_id = user['user_id']
40 73 repo_id = user_util.create_repo().repo_id
41 store_path = self.app._pyramid_settings[config_keys.store_path]
74
75 config = ini_settings
76 config[config_keys.backend_type] = config_keys.backend_legacy_filesystem
77
42 78 store_uid = fid
43 79
44 80 if exists:
45 81 status = 200
46 store = utils.get_file_storage({config_keys.store_path: store_path})
82 f_store = store_utils.get_filestore_backend(config)
47 83 filesystem_file = os.path.join(str(tmpdir), fid)
48 84 with open(filesystem_file, 'wt') as f:
49 85 f.write(content)
50 86
51 87 with open(filesystem_file, 'rb') as f:
52 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
88 store_uid, metadata = f_store.store(fid, f, metadata={'filename': fid})
89 os.remove(filesystem_file)
53 90
54 91 entry = FileStore.create(
55 92 file_uid=store_uid, filename=metadata["filename"],
56 93 file_hash=metadata["sha256"], file_size=metadata["size"],
57 94 file_display_name='file_display_name',
58 95 file_description='repo artifact `{}`'.format(metadata["filename"]),
59 96 check_acl=True, user_id=user_id,
60 97 scope_repo_id=repo_id
61 98 )
62 99 Session().add(entry)
63 100 Session().commit()
64 101
65 102 else:
66 103 status = 404
67 104
68 105 response = self.app.get(route_path('download_file', fid=store_uid), status=status)
69 106
70 107 if exists:
71 108 assert response.text == content
72 file_store_path = os.path.dirname(store.resolve_name(store_uid, store_path)[1])
73 metadata_file = os.path.join(file_store_path, store_uid + '.meta')
74 assert os.path.exists(metadata_file)
75 with open(metadata_file, 'rb') as f:
76 json_data = json.loads(f.read())
77 109
78 assert json_data
79 assert 'size' in json_data
110 metadata = f_store.get_metadata(store_uid)
111
112 assert 'size' in metadata
80 113
81 114 def test_upload_files_without_content_to_store(self):
82 115 self.log_user()
83 116 response = self.app.post(
84 117 route_path('upload_file'),
85 118 params={'csrf_token': self.csrf_token},
86 119 status=200)
87 120
88 121 assert response.json == {
89 122 'error': 'store_file data field is missing',
90 123 'access_path': None,
91 124 'store_fid': None}
92 125
93 126 def test_upload_files_bogus_content_to_store(self):
94 127 self.log_user()
95 128 response = self.app.post(
96 129 route_path('upload_file'),
97 130 params={'csrf_token': self.csrf_token, 'store_file': 'bogus'},
98 131 status=200)
99 132
100 133 assert response.json == {
101 134 'error': 'filename cannot be read from the data field',
102 135 'access_path': None,
103 136 'store_fid': None}
104 137
105 138 def test_upload_content_to_store(self):
106 139 self.log_user()
107 140 response = self.app.post(
108 141 route_path('upload_file'),
109 142 upload_files=[('store_file', b'myfile.txt', b'SOME CONTENT')],
110 143 params={'csrf_token': self.csrf_token},
111 144 status=200)
112 145
113 146 assert response.json['store_fid']
114 147
115 @pytest.fixture()
116 def create_artifact_factory(self, tmpdir):
117 def factory(user_id, content):
118 store_path = self.app._pyramid_settings[config_keys.store_path]
119 store = utils.get_file_storage({config_keys.store_path: store_path})
120 fid = 'example.txt'
121
122 filesystem_file = os.path.join(str(tmpdir), fid)
123 with open(filesystem_file, 'wt') as f:
124 f.write(content)
125
126 with open(filesystem_file, 'rb') as f:
127 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
128
129 entry = FileStore.create(
130 file_uid=store_uid, filename=metadata["filename"],
131 file_hash=metadata["sha256"], file_size=metadata["size"],
132 file_display_name='file_display_name',
133 file_description='repo artifact `{}`'.format(metadata["filename"]),
134 check_acl=True, user_id=user_id,
135 )
136 Session().add(entry)
137 Session().commit()
138 return entry
139 return factory
140
141 148 def test_download_file_non_scoped(self, user_util, create_artifact_factory):
142 149 user = self.log_user()
143 150 user_id = user['user_id']
144 151 content = 'HELLO MY NAME IS ARTIFACT !'
145 152
146 153 artifact = create_artifact_factory(user_id, content)
147 154 file_uid = artifact.file_uid
148 155 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
149 156 assert response.text == content
150 157
151 158 # log-in to new user and test download again
152 159 user = user_util.create_user(password='qweqwe')
153 160 self.log_user(user.username, 'qweqwe')
154 161 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
155 162 assert response.text == content
156 163
157 164 def test_download_file_scoped_to_repo(self, user_util, create_artifact_factory):
158 165 user = self.log_user()
159 166 user_id = user['user_id']
160 167 content = 'HELLO MY NAME IS ARTIFACT !'
161 168
162 169 artifact = create_artifact_factory(user_id, content)
163 170 # bind to repo
164 171 repo = user_util.create_repo()
165 172 repo_id = repo.repo_id
166 173 artifact.scope_repo_id = repo_id
167 174 Session().add(artifact)
168 175 Session().commit()
169 176
170 177 file_uid = artifact.file_uid
171 178 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
172 179 assert response.text == content
173 180
174 181 # log-in to new user and test download again
175 182 user = user_util.create_user(password='qweqwe')
176 183 self.log_user(user.username, 'qweqwe')
177 184 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
178 185 assert response.text == content
179 186
180 187 # forbid user the rights to repo
181 188 repo = Repository.get(repo_id)
182 189 user_util.grant_user_permission_to_repo(repo, user, 'repository.none')
183 190 self.app.get(route_path('download_file', fid=file_uid), status=404)
184 191
185 192 def test_download_file_scoped_to_user(self, user_util, create_artifact_factory):
186 193 user = self.log_user()
187 194 user_id = user['user_id']
188 195 content = 'HELLO MY NAME IS ARTIFACT !'
189 196
190 197 artifact = create_artifact_factory(user_id, content)
191 198 # bind to user
192 199 user = user_util.create_user(password='qweqwe')
193 200
194 201 artifact.scope_user_id = user.user_id
195 202 Session().add(artifact)
196 203 Session().commit()
197 204
198 205 # artifact creator doesn't have access since it's bind to another user
199 206 file_uid = artifact.file_uid
200 207 self.app.get(route_path('download_file', fid=file_uid), status=404)
201 208
202 209 # log-in to new user and test download again, should be ok since we're bind to this artifact
203 210 self.log_user(user.username, 'qweqwe')
204 211 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
205 212 assert response.text == content
206 213
207 214 def test_download_file_scoped_to_repo_with_bad_token(self, user_util, create_artifact_factory):
208 215 user_id = User.get_first_super_admin().user_id
209 216 content = 'HELLO MY NAME IS ARTIFACT !'
210 217
211 218 artifact = create_artifact_factory(user_id, content)
212 219 # bind to repo
213 220 repo = user_util.create_repo()
214 221 repo_id = repo.repo_id
215 222 artifact.scope_repo_id = repo_id
216 223 Session().add(artifact)
217 224 Session().commit()
218 225
219 226 file_uid = artifact.file_uid
220 227 self.app.get(route_path('download_file_by_token',
221 228 _auth_token='bogus', fid=file_uid), status=302)
222 229
223 230 def test_download_file_scoped_to_repo_with_token(self, user_util, create_artifact_factory):
224 231 user = User.get_first_super_admin()
225 232 AuthTokenModel().create(user, 'test artifact token',
226 233 role=AuthTokenModel.cls.ROLE_ARTIFACT_DOWNLOAD)
227 234
228 235 user = User.get_first_super_admin()
229 236 artifact_token = user.artifact_token
230 237
231 238 user_id = User.get_first_super_admin().user_id
232 239 content = 'HELLO MY NAME IS ARTIFACT !'
233 240
234 241 artifact = create_artifact_factory(user_id, content)
235 242 # bind to repo
236 243 repo = user_util.create_repo()
237 244 repo_id = repo.repo_id
238 245 artifact.scope_repo_id = repo_id
239 246 Session().add(artifact)
240 247 Session().commit()
241 248
242 249 file_uid = artifact.file_uid
243 250 response = self.app.get(
244 251 route_path('download_file_by_token',
245 252 _auth_token=artifact_token, fid=file_uid), status=200)
246 253 assert response.text == content
@@ -1,55 +1,145 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import io
20 20 import uuid
21 21 import pathlib
22 import s3fs
23
24 from rhodecode.lib.hash_utils import sha256_safe
25 from rhodecode.apps.file_store import config_keys
26
27
28 file_store_meta = None
29
30
31 def get_filestore_config(config) -> dict:
32
33 final_config = {}
34
35 for k, v in config.items():
36 if k.startswith('file_store'):
37 final_config[k] = v
38
39 return final_config
22 40
23 41
24 def get_file_storage(settings):
25 from rhodecode.apps.file_store.backends.local_store import LocalFileStorage
26 from rhodecode.apps.file_store import config_keys
27 store_path = settings.get(config_keys.store_path)
28 return LocalFileStorage(base_path=store_path)
42 def get_filestore_backend(config, always_init=False):
43 """
44
45 usage::
46 from rhodecode.apps.file_store import get_filestore_backend
47 f_store = get_filestore_backend(config=CONFIG)
48
49 :param config:
50 :param always_init:
51 :return:
52 """
53
54 global file_store_meta
55 if file_store_meta is not None and not always_init:
56 return file_store_meta
57
58 config = get_filestore_config(config)
59 backend = config[config_keys.backend_type]
60
61 match backend:
62 case config_keys.backend_legacy_filesystem:
63 # Legacy backward compatible storage
64 from rhodecode.apps.file_store.backends.filesystem_legacy import LegacyFileSystemBackend
65 d_cache = LegacyFileSystemBackend(
66 settings=config
67 )
68 case config_keys.backend_filesystem:
69 from rhodecode.apps.file_store.backends.filesystem import FileSystemBackend
70 d_cache = FileSystemBackend(
71 settings=config
72 )
73 case config_keys.backend_objectstore:
74 from rhodecode.apps.file_store.backends.objectstore import ObjectStoreBackend
75 d_cache = ObjectStoreBackend(
76 settings=config
77 )
78 case _:
79 raise ValueError(
80 f'file_store.backend.type only supports "{config_keys.backend_types}" got {backend}'
81 )
82
83 cache_meta = d_cache
84 return cache_meta
29 85
30 86
31 87 def splitext(filename):
32 ext = ''.join(pathlib.Path(filename).suffixes)
88 final_ext = []
89 for suffix in pathlib.Path(filename).suffixes:
90 if not suffix.isascii():
91 continue
92
93 suffix = " ".join(suffix.split()).replace(" ", "")
94 final_ext.append(suffix)
95 ext = ''.join(final_ext)
33 96 return filename, ext
34 97
35 98
36 def uid_filename(filename, randomized=True):
99 def get_uid_filename(filename, randomized=True):
37 100 """
38 101 Generates a randomized or stable (uuid) filename,
39 102 preserving the original extension.
40 103
41 104 :param filename: the original filename
42 105 :param randomized: define if filename should be stable (sha1 based) or randomized
43 106 """
44 107
45 108 _, ext = splitext(filename)
46 109 if randomized:
47 110 uid = uuid.uuid4()
48 111 else:
49 hash_key = '{}.{}'.format(filename, 'store')
112 store_suffix = "store"
113 hash_key = f'{filename}.{store_suffix}'
50 114 uid = uuid.uuid5(uuid.NAMESPACE_URL, hash_key)
51 115 return str(uid) + ext.lower()
52 116
53 117
54 118 def bytes_to_file_obj(bytes_data):
55 return io.StringIO(bytes_data)
119 return io.BytesIO(bytes_data)
120
121
122 class ShardFileReader:
123
124 def __init__(self, file_like_reader):
125 self._file_like_reader = file_like_reader
126
127 def __getattr__(self, item):
128 if isinstance(self._file_like_reader, s3fs.core.S3File):
129 match item:
130 case 'name':
131 # S3 FileWrapper doesn't support name attribute, and we use it
132 return self._file_like_reader.full_name
133 case _:
134 return getattr(self._file_like_reader, item)
135 else:
136 return getattr(self._file_like_reader, item)
137
138
139 def archive_iterator(_reader, block_size: int = 4096 * 512):
140 # 4096 * 64 = 64KB
141 while 1:
142 data = _reader.read(block_size)
143 if not data:
144 break
145 yield data
@@ -1,200 +1,197 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18 import logging
19 19
20
21 from pyramid.response import FileResponse
20 from pyramid.response import Response
22 21 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
23 22
24 23 from rhodecode.apps._base import BaseAppView
25 from rhodecode.apps.file_store import utils
24 from rhodecode.apps.file_store import utils as store_utils
26 25 from rhodecode.apps.file_store.exceptions import (
27 26 FileNotAllowedException, FileOverSizeException)
28 27
29 28 from rhodecode.lib import helpers as h
30 29 from rhodecode.lib import audit_logger
31 30 from rhodecode.lib.auth import (
32 31 CSRFRequired, NotAnonymous, HasRepoPermissionAny, HasRepoGroupPermissionAny,
33 32 LoginRequired)
33 from rhodecode.lib.str_utils import header_safe_str
34 34 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
35 35 from rhodecode.model.db import Session, FileStore, UserApiKeys
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 class FileStoreView(BaseAppView):
41 41 upload_key = 'store_file'
42 42
43 43 def load_default_context(self):
44 44 c = self._get_local_tmpl_context()
45 self.storage = utils.get_file_storage(self.request.registry.settings)
45 self.f_store = store_utils.get_filestore_backend(self.request.registry.settings)
46 46 return c
47 47
48 48 def _guess_type(self, file_name):
49 49 """
50 50 Our own type guesser for mimetypes using the rich DB
51 51 """
52 52 if not hasattr(self, 'db'):
53 53 self.db = get_mimetypes_db()
54 54 _content_type, _encoding = self.db.guess_type(file_name, strict=False)
55 55 return _content_type, _encoding
56 56
57 57 def _serve_file(self, file_uid):
58 if not self.storage.exists(file_uid):
59 store_path = self.storage.store_path(file_uid)
60 log.debug('File with FID:%s not found in the store under `%s`',
61 file_uid, store_path)
58 if not self.f_store.filename_exists(file_uid):
59 store_path = self.f_store.store_path(file_uid)
60 log.warning('File with FID:%s not found in the store under `%s`',
61 file_uid, store_path)
62 62 raise HTTPNotFound()
63 63
64 64 db_obj = FileStore.get_by_store_uid(file_uid, safe=True)
65 65 if not db_obj:
66 66 raise HTTPNotFound()
67 67
68 68 # private upload for user
69 69 if db_obj.check_acl and db_obj.scope_user_id:
70 70 log.debug('Artifact: checking scope access for bound artifact user: `%s`',
71 71 db_obj.scope_user_id)
72 72 user = db_obj.user
73 73 if self._rhodecode_db_user.user_id != user.user_id:
74 74 log.warning('Access to file store object forbidden')
75 75 raise HTTPNotFound()
76 76
77 77 # scoped to repository permissions
78 78 if db_obj.check_acl and db_obj.scope_repo_id:
79 79 log.debug('Artifact: checking scope access for bound artifact repo: `%s`',
80 80 db_obj.scope_repo_id)
81 81 repo = db_obj.repo
82 82 perm_set = ['repository.read', 'repository.write', 'repository.admin']
83 83 has_perm = HasRepoPermissionAny(*perm_set)(repo.repo_name, 'FileStore check')
84 84 if not has_perm:
85 85 log.warning('Access to file store object `%s` forbidden', file_uid)
86 86 raise HTTPNotFound()
87 87
88 88 # scoped to repository group permissions
89 89 if db_obj.check_acl and db_obj.scope_repo_group_id:
90 90 log.debug('Artifact: checking scope access for bound artifact repo group: `%s`',
91 91 db_obj.scope_repo_group_id)
92 92 repo_group = db_obj.repo_group
93 93 perm_set = ['group.read', 'group.write', 'group.admin']
94 94 has_perm = HasRepoGroupPermissionAny(*perm_set)(repo_group.group_name, 'FileStore check')
95 95 if not has_perm:
96 96 log.warning('Access to file store object `%s` forbidden', file_uid)
97 97 raise HTTPNotFound()
98 98
99 99 FileStore.bump_access_counter(file_uid)
100 100
101 file_path = self.storage.store_path(file_uid)
101 file_name = db_obj.file_display_name
102 102 content_type = 'application/octet-stream'
103 content_encoding = None
104 103
105 _content_type, _encoding = self._guess_type(file_path)
104 _content_type, _encoding = self._guess_type(file_name)
106 105 if _content_type:
107 106 content_type = _content_type
108 107
109 108 # For file store we don't submit any session data, this logic tells the
110 109 # Session lib to skip it
111 110 setattr(self.request, '_file_response', True)
112 response = FileResponse(
113 file_path, request=self.request,
114 content_type=content_type, content_encoding=content_encoding)
111 reader, _meta = self.f_store.fetch(file_uid)
115 112
116 file_name = db_obj.file_display_name
113 response = Response(app_iter=store_utils.archive_iterator(reader))
117 114
118 response.headers["Content-Disposition"] = (
119 f'attachment; filename="{str(file_name)}"'
120 )
115 response.content_type = str(content_type)
116 response.content_disposition = f'attachment; filename="{header_safe_str(file_name)}"'
117
121 118 response.headers["X-RC-Artifact-Id"] = str(db_obj.file_store_id)
122 response.headers["X-RC-Artifact-Desc"] = str(db_obj.file_description)
119 response.headers["X-RC-Artifact-Desc"] = header_safe_str(db_obj.file_description)
123 120 response.headers["X-RC-Artifact-Sha256"] = str(db_obj.file_hash)
124 121 return response
125 122
126 123 @LoginRequired()
127 124 @NotAnonymous()
128 125 @CSRFRequired()
129 126 def upload_file(self):
130 127 self.load_default_context()
131 128 file_obj = self.request.POST.get(self.upload_key)
132 129
133 130 if file_obj is None:
134 131 return {'store_fid': None,
135 132 'access_path': None,
136 133 'error': f'{self.upload_key} data field is missing'}
137 134
138 135 if not hasattr(file_obj, 'filename'):
139 136 return {'store_fid': None,
140 137 'access_path': None,
141 138 'error': 'filename cannot be read from the data field'}
142 139
143 140 filename = file_obj.filename
144 141
145 142 metadata = {
146 143 'user_uploaded': {'username': self._rhodecode_user.username,
147 144 'user_id': self._rhodecode_user.user_id,
148 145 'ip': self._rhodecode_user.ip_addr}}
149 146 try:
150 store_uid, metadata = self.storage.save_file(
151 file_obj.file, filename, extra_metadata=metadata)
147 store_uid, metadata = self.f_store.store(
148 filename, file_obj.file, extra_metadata=metadata)
152 149 except FileNotAllowedException:
153 150 return {'store_fid': None,
154 151 'access_path': None,
155 152 'error': f'File {filename} is not allowed.'}
156 153
157 154 except FileOverSizeException:
158 155 return {'store_fid': None,
159 156 'access_path': None,
160 157 'error': f'File {filename} is exceeding allowed limit.'}
161 158
162 159 try:
163 160 entry = FileStore.create(
164 161 file_uid=store_uid, filename=metadata["filename"],
165 162 file_hash=metadata["sha256"], file_size=metadata["size"],
166 163 file_description='upload attachment',
167 164 check_acl=False, user_id=self._rhodecode_user.user_id
168 165 )
169 166 Session().add(entry)
170 167 Session().commit()
171 168 log.debug('Stored upload in DB as %s', entry)
172 169 except Exception:
173 170 log.exception('Failed to store file %s', filename)
174 171 return {'store_fid': None,
175 172 'access_path': None,
176 173 'error': f'File {filename} failed to store in DB.'}
177 174
178 175 return {'store_fid': store_uid,
179 176 'access_path': h.route_path('download_file', fid=store_uid)}
180 177
181 178 # ACL is checked by scopes, if no scope the file is accessible to all
182 179 def download_file(self):
183 180 self.load_default_context()
184 181 file_uid = self.request.matchdict['fid']
185 log.debug('Requesting FID:%s from store %s', file_uid, self.storage)
182 log.debug('Requesting FID:%s from store %s', file_uid, self.f_store)
186 183 return self._serve_file(file_uid)
187 184
188 185 # in addition to @LoginRequired ACL is checked by scopes
189 186 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_ARTIFACT_DOWNLOAD])
190 187 @NotAnonymous()
191 188 def download_file_by_token(self):
192 189 """
193 190 Special view that allows to access the download file by special URL that
194 191 is stored inside the URL.
195 192
196 193 http://example.com/_file_store/token-download/TOKEN/FILE_UID
197 194 """
198 195 self.load_default_context()
199 196 file_uid = self.request.matchdict['fid']
200 197 return self._serve_file(file_uid)
@@ -1,830 +1,830 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import logging
20 20 import collections
21 21
22 22 from pyramid.httpexceptions import (
23 23 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
24 24 from pyramid.renderers import render
25 25 from pyramid.response import Response
26 26
27 27 from rhodecode.apps._base import RepoAppView
28 28 from rhodecode.apps.file_store import utils as store_utils
29 29 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
30 30
31 31 from rhodecode.lib import diffs, codeblocks, channelstream
32 32 from rhodecode.lib.auth import (
33 33 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
34 34 from rhodecode.lib import ext_json
35 35 from collections import OrderedDict
36 36 from rhodecode.lib.diffs import (
37 37 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
38 38 get_diff_whitespace_flag)
39 39 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
40 40 import rhodecode.lib.helpers as h
41 41 from rhodecode.lib.utils2 import str2bool, StrictAttributeDict, safe_str
42 42 from rhodecode.lib.vcs.backends.base import EmptyCommit
43 43 from rhodecode.lib.vcs.exceptions import (
44 44 RepositoryError, CommitDoesNotExistError)
45 45 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
46 46 ChangesetCommentHistory
47 47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 48 from rhodecode.model.comment import CommentsModel
49 49 from rhodecode.model.meta import Session
50 50 from rhodecode.model.settings import VcsSettingsModel
51 51
52 52 log = logging.getLogger(__name__)
53 53
54 54
55 55 def _update_with_GET(params, request):
56 56 for k in ['diff1', 'diff2', 'diff']:
57 57 params[k] += request.GET.getall(k)
58 58
59 59
60 60 class RepoCommitsView(RepoAppView):
61 61 def load_default_context(self):
62 62 c = self._get_local_tmpl_context(include_app_defaults=True)
63 63 c.rhodecode_repo = self.rhodecode_vcs_repo
64 64
65 65 return c
66 66
67 67 def _is_diff_cache_enabled(self, target_repo):
68 68 caching_enabled = self._get_general_setting(
69 69 target_repo, 'rhodecode_diff_cache')
70 70 log.debug('Diff caching enabled: %s', caching_enabled)
71 71 return caching_enabled
72 72
73 73 def _commit(self, commit_id_range, method):
74 74 _ = self.request.translate
75 75 c = self.load_default_context()
76 76 c.fulldiff = self.request.GET.get('fulldiff')
77 77 redirect_to_combined = str2bool(self.request.GET.get('redirect_combined'))
78 78
79 79 # fetch global flags of ignore ws or context lines
80 80 diff_context = get_diff_context(self.request)
81 81 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
82 82
83 83 # diff_limit will cut off the whole diff if the limit is applied
84 84 # otherwise it will just hide the big files from the front-end
85 85 diff_limit = c.visual.cut_off_limit_diff
86 86 file_limit = c.visual.cut_off_limit_file
87 87
88 88 # get ranges of commit ids if preset
89 89 commit_range = commit_id_range.split('...')[:2]
90 90
91 91 try:
92 92 pre_load = ['affected_files', 'author', 'branch', 'date',
93 93 'message', 'parents']
94 94 if self.rhodecode_vcs_repo.alias == 'hg':
95 95 pre_load += ['hidden', 'obsolete', 'phase']
96 96
97 97 if len(commit_range) == 2:
98 98 commits = self.rhodecode_vcs_repo.get_commits(
99 99 start_id=commit_range[0], end_id=commit_range[1],
100 100 pre_load=pre_load, translate_tags=False)
101 101 commits = list(commits)
102 102 else:
103 103 commits = [self.rhodecode_vcs_repo.get_commit(
104 104 commit_id=commit_id_range, pre_load=pre_load)]
105 105
106 106 c.commit_ranges = commits
107 107 if not c.commit_ranges:
108 108 raise RepositoryError('The commit range returned an empty result')
109 109 except CommitDoesNotExistError as e:
110 110 msg = _('No such commit exists. Org exception: `{}`').format(safe_str(e))
111 111 h.flash(msg, category='error')
112 112 raise HTTPNotFound()
113 113 except Exception:
114 114 log.exception("General failure")
115 115 raise HTTPNotFound()
116 116 single_commit = len(c.commit_ranges) == 1
117 117
118 118 if redirect_to_combined and not single_commit:
119 119 source_ref = getattr(c.commit_ranges[0].parents[0]
120 120 if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id')
121 121 target_ref = c.commit_ranges[-1].raw_id
122 122 next_url = h.route_path(
123 123 'repo_compare',
124 124 repo_name=c.repo_name,
125 125 source_ref_type='rev',
126 126 source_ref=source_ref,
127 127 target_ref_type='rev',
128 128 target_ref=target_ref)
129 129 raise HTTPFound(next_url)
130 130
131 131 c.changes = OrderedDict()
132 132 c.lines_added = 0
133 133 c.lines_deleted = 0
134 134
135 135 # auto collapse if we have more than limit
136 136 collapse_limit = diffs.DiffProcessor._collapse_commits_over
137 137 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
138 138
139 139 c.commit_statuses = ChangesetStatus.STATUSES
140 140 c.inline_comments = []
141 141 c.files = []
142 142
143 143 c.comments = []
144 144 c.unresolved_comments = []
145 145 c.resolved_comments = []
146 146
147 147 # Single commit
148 148 if single_commit:
149 149 commit = c.commit_ranges[0]
150 150 c.comments = CommentsModel().get_comments(
151 151 self.db_repo.repo_id,
152 152 revision=commit.raw_id)
153 153
154 154 # comments from PR
155 155 statuses = ChangesetStatusModel().get_statuses(
156 156 self.db_repo.repo_id, commit.raw_id,
157 157 with_revisions=True)
158 158
159 159 prs = set()
160 160 reviewers = list()
161 161 reviewers_duplicates = set() # to not have duplicates from multiple votes
162 162 for c_status in statuses:
163 163
164 164 # extract associated pull-requests from votes
165 165 if c_status.pull_request:
166 166 prs.add(c_status.pull_request)
167 167
168 168 # extract reviewers
169 169 _user_id = c_status.author.user_id
170 170 if _user_id not in reviewers_duplicates:
171 171 reviewers.append(
172 172 StrictAttributeDict({
173 173 'user': c_status.author,
174 174
175 175 # fake attributed for commit, page that we don't have
176 176 # but we share the display with PR page
177 177 'mandatory': False,
178 178 'reasons': [],
179 179 'rule_user_group_data': lambda: None
180 180 })
181 181 )
182 182 reviewers_duplicates.add(_user_id)
183 183
184 184 c.reviewers_count = len(reviewers)
185 185 c.observers_count = 0
186 186
187 187 # from associated statuses, check the pull requests, and
188 188 # show comments from them
189 189 for pr in prs:
190 190 c.comments.extend(pr.comments)
191 191
192 192 c.unresolved_comments = CommentsModel()\
193 193 .get_commit_unresolved_todos(commit.raw_id)
194 194 c.resolved_comments = CommentsModel()\
195 195 .get_commit_resolved_todos(commit.raw_id)
196 196
197 197 c.inline_comments_flat = CommentsModel()\
198 198 .get_commit_inline_comments(commit.raw_id)
199 199
200 200 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
201 201 statuses, reviewers)
202 202
203 203 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
204 204
205 205 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
206 206
207 207 for review_obj, member, reasons, mandatory, status in review_statuses:
208 208 member_reviewer = h.reviewer_as_json(
209 209 member, reasons=reasons, mandatory=mandatory, role=None,
210 210 user_group=None
211 211 )
212 212
213 213 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
214 214 member_reviewer['review_status'] = current_review_status
215 215 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
216 216 member_reviewer['allowed_to_update'] = False
217 217 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
218 218
219 219 c.commit_set_reviewers_data_json = ext_json.str_json(c.commit_set_reviewers_data_json)
220 220
221 221 # NOTE(marcink): this uses the same voting logic as in pull-requests
222 222 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
223 223 c.commit_broadcast_channel = channelstream.comment_channel(c.repo_name, commit_obj=commit)
224 224
225 225 diff = None
226 226 # Iterate over ranges (default commit view is always one commit)
227 227 for commit in c.commit_ranges:
228 228 c.changes[commit.raw_id] = []
229 229
230 230 commit2 = commit
231 231 commit1 = commit.first_parent
232 232
233 233 if method == 'show':
234 234 inline_comments = CommentsModel().get_inline_comments(
235 235 self.db_repo.repo_id, revision=commit.raw_id)
236 236 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
237 237 inline_comments))
238 238 c.inline_comments = inline_comments
239 239
240 240 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
241 241 self.db_repo)
242 242 cache_file_path = diff_cache_exist(
243 243 cache_path, 'diff', commit.raw_id,
244 244 hide_whitespace_changes, diff_context, c.fulldiff)
245 245
246 246 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
247 247 force_recache = str2bool(self.request.GET.get('force_recache'))
248 248
249 249 cached_diff = None
250 250 if caching_enabled:
251 251 cached_diff = load_cached_diff(cache_file_path)
252 252
253 253 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
254 254 if not force_recache and has_proper_diff_cache:
255 255 diffset = cached_diff['diff']
256 256 else:
257 257 vcs_diff = self.rhodecode_vcs_repo.get_diff(
258 258 commit1, commit2,
259 259 ignore_whitespace=hide_whitespace_changes,
260 260 context=diff_context)
261 261
262 262 diff_processor = diffs.DiffProcessor(vcs_diff, diff_format='newdiff',
263 263 diff_limit=diff_limit,
264 264 file_limit=file_limit,
265 265 show_full_diff=c.fulldiff)
266 266
267 267 _parsed = diff_processor.prepare()
268 268
269 269 diffset = codeblocks.DiffSet(
270 270 repo_name=self.db_repo_name,
271 271 source_node_getter=codeblocks.diffset_node_getter(commit1),
272 272 target_node_getter=codeblocks.diffset_node_getter(commit2))
273 273
274 274 diffset = self.path_filter.render_patchset_filtered(
275 275 diffset, _parsed, commit1.raw_id, commit2.raw_id)
276 276
277 277 # save cached diff
278 278 if caching_enabled:
279 279 cache_diff(cache_file_path, diffset, None)
280 280
281 281 c.limited_diff = diffset.limited_diff
282 282 c.changes[commit.raw_id] = diffset
283 283 else:
284 284 # TODO(marcink): no cache usage here...
285 285 _diff = self.rhodecode_vcs_repo.get_diff(
286 286 commit1, commit2,
287 287 ignore_whitespace=hide_whitespace_changes, context=diff_context)
288 288 diff_processor = diffs.DiffProcessor(_diff, diff_format='newdiff',
289 289 diff_limit=diff_limit,
290 290 file_limit=file_limit, show_full_diff=c.fulldiff)
291 291 # downloads/raw we only need RAW diff nothing else
292 292 diff = self.path_filter.get_raw_patch(diff_processor)
293 293 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
294 294
295 295 # sort comments by how they were generated
296 296 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
297 297 c.at_version_num = None
298 298
299 299 if len(c.commit_ranges) == 1:
300 300 c.commit = c.commit_ranges[0]
301 301 c.parent_tmpl = ''.join(
302 302 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
303 303
304 304 if method == 'download':
305 305 response = Response(diff)
306 306 response.content_type = 'text/plain'
307 307 response.content_disposition = (
308 308 'attachment; filename=%s.diff' % commit_id_range[:12])
309 309 return response
310 310 elif method == 'patch':
311 311
312 312 c.diff = safe_str(diff)
313 313 patch = render(
314 314 'rhodecode:templates/changeset/patch_changeset.mako',
315 315 self._get_template_context(c), self.request)
316 316 response = Response(patch)
317 317 response.content_type = 'text/plain'
318 318 return response
319 319 elif method == 'raw':
320 320 response = Response(diff)
321 321 response.content_type = 'text/plain'
322 322 return response
323 323 elif method == 'show':
324 324 if len(c.commit_ranges) == 1:
325 325 html = render(
326 326 'rhodecode:templates/changeset/changeset.mako',
327 327 self._get_template_context(c), self.request)
328 328 return Response(html)
329 329 else:
330 330 c.ancestor = None
331 331 c.target_repo = self.db_repo
332 332 html = render(
333 333 'rhodecode:templates/changeset/changeset_range.mako',
334 334 self._get_template_context(c), self.request)
335 335 return Response(html)
336 336
337 337 raise HTTPBadRequest()
338 338
339 339 @LoginRequired()
340 340 @HasRepoPermissionAnyDecorator(
341 341 'repository.read', 'repository.write', 'repository.admin')
342 342 def repo_commit_show(self):
343 343 commit_id = self.request.matchdict['commit_id']
344 344 return self._commit(commit_id, method='show')
345 345
346 346 @LoginRequired()
347 347 @HasRepoPermissionAnyDecorator(
348 348 'repository.read', 'repository.write', 'repository.admin')
349 349 def repo_commit_raw(self):
350 350 commit_id = self.request.matchdict['commit_id']
351 351 return self._commit(commit_id, method='raw')
352 352
353 353 @LoginRequired()
354 354 @HasRepoPermissionAnyDecorator(
355 355 'repository.read', 'repository.write', 'repository.admin')
356 356 def repo_commit_patch(self):
357 357 commit_id = self.request.matchdict['commit_id']
358 358 return self._commit(commit_id, method='patch')
359 359
360 360 @LoginRequired()
361 361 @HasRepoPermissionAnyDecorator(
362 362 'repository.read', 'repository.write', 'repository.admin')
363 363 def repo_commit_download(self):
364 364 commit_id = self.request.matchdict['commit_id']
365 365 return self._commit(commit_id, method='download')
366 366
367 367 def _commit_comments_create(self, commit_id, comments):
368 368 _ = self.request.translate
369 369 data = {}
370 370 if not comments:
371 371 return
372 372
373 373 commit = self.db_repo.get_commit(commit_id)
374 374
375 375 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
376 376 for entry in comments:
377 377 c = self.load_default_context()
378 378 comment_type = entry['comment_type']
379 379 text = entry['text']
380 380 status = entry['status']
381 381 is_draft = str2bool(entry['is_draft'])
382 382 resolves_comment_id = entry['resolves_comment_id']
383 383 f_path = entry['f_path']
384 384 line_no = entry['line']
385 385 target_elem_id = f'file-{h.safeid(h.safe_str(f_path))}'
386 386
387 387 if status:
388 388 text = text or (_('Status change %(transition_icon)s %(status)s')
389 389 % {'transition_icon': '>',
390 390 'status': ChangesetStatus.get_status_lbl(status)})
391 391
392 392 comment = CommentsModel().create(
393 393 text=text,
394 394 repo=self.db_repo.repo_id,
395 395 user=self._rhodecode_db_user.user_id,
396 396 commit_id=commit_id,
397 397 f_path=f_path,
398 398 line_no=line_no,
399 399 status_change=(ChangesetStatus.get_status_lbl(status)
400 400 if status else None),
401 401 status_change_type=status,
402 402 comment_type=comment_type,
403 403 is_draft=is_draft,
404 404 resolves_comment_id=resolves_comment_id,
405 405 auth_user=self._rhodecode_user,
406 406 send_email=not is_draft, # skip notification for draft comments
407 407 )
408 408 is_inline = comment.is_inline
409 409
410 410 # get status if set !
411 411 if status:
412 412 # `dont_allow_on_closed_pull_request = True` means
413 413 # if latest status was from pull request and it's closed
414 414 # disallow changing status !
415 415
416 416 try:
417 417 ChangesetStatusModel().set_status(
418 418 self.db_repo.repo_id,
419 419 status,
420 420 self._rhodecode_db_user.user_id,
421 421 comment,
422 422 revision=commit_id,
423 423 dont_allow_on_closed_pull_request=True
424 424 )
425 425 except StatusChangeOnClosedPullRequestError:
426 426 msg = _('Changing the status of a commit associated with '
427 427 'a closed pull request is not allowed')
428 428 log.exception(msg)
429 429 h.flash(msg, category='warning')
430 430 raise HTTPFound(h.route_path(
431 431 'repo_commit', repo_name=self.db_repo_name,
432 432 commit_id=commit_id))
433 433
434 434 Session().flush()
435 435 # this is somehow required to get access to some relationship
436 436 # loaded on comment
437 437 Session().refresh(comment)
438 438
439 439 # skip notifications for drafts
440 440 if not is_draft:
441 441 CommentsModel().trigger_commit_comment_hook(
442 442 self.db_repo, self._rhodecode_user, 'create',
443 443 data={'comment': comment, 'commit': commit})
444 444
445 445 comment_id = comment.comment_id
446 446 data[comment_id] = {
447 447 'target_id': target_elem_id
448 448 }
449 449 Session().flush()
450 450
451 451 c.co = comment
452 452 c.at_version_num = 0
453 453 c.is_new = True
454 454 rendered_comment = render(
455 455 'rhodecode:templates/changeset/changeset_comment_block.mako',
456 456 self._get_template_context(c), self.request)
457 457
458 458 data[comment_id].update(comment.get_dict())
459 459 data[comment_id].update({'rendered_text': rendered_comment})
460 460
461 461 # finalize, commit and redirect
462 462 Session().commit()
463 463
464 464 # skip channelstream for draft comments
465 465 if not all_drafts:
466 466 comment_broadcast_channel = channelstream.comment_channel(
467 467 self.db_repo_name, commit_obj=commit)
468 468
469 469 comment_data = data
470 470 posted_comment_type = 'inline' if is_inline else 'general'
471 471 if len(data) == 1:
472 472 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
473 473 else:
474 474 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
475 475
476 476 channelstream.comment_channelstream_push(
477 477 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
478 478 comment_data=comment_data)
479 479
480 480 return data
481 481
482 482 @LoginRequired()
483 483 @NotAnonymous()
484 484 @HasRepoPermissionAnyDecorator(
485 485 'repository.read', 'repository.write', 'repository.admin')
486 486 @CSRFRequired()
487 487 def repo_commit_comment_create(self):
488 488 _ = self.request.translate
489 489 commit_id = self.request.matchdict['commit_id']
490 490
491 491 multi_commit_ids = []
492 492 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
493 493 if _commit_id not in ['', None, EmptyCommit.raw_id]:
494 494 if _commit_id not in multi_commit_ids:
495 495 multi_commit_ids.append(_commit_id)
496 496
497 497 commit_ids = multi_commit_ids or [commit_id]
498 498
499 499 data = []
500 500 # Multiple comments for each passed commit id
501 501 for current_id in filter(None, commit_ids):
502 502 comment_data = {
503 503 'comment_type': self.request.POST.get('comment_type'),
504 504 'text': self.request.POST.get('text'),
505 505 'status': self.request.POST.get('changeset_status', None),
506 506 'is_draft': self.request.POST.get('draft'),
507 507 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
508 508 'close_pull_request': self.request.POST.get('close_pull_request'),
509 509 'f_path': self.request.POST.get('f_path'),
510 510 'line': self.request.POST.get('line'),
511 511 }
512 512 comment = self._commit_comments_create(commit_id=current_id, comments=[comment_data])
513 513 data.append(comment)
514 514
515 515 return data if len(data) > 1 else data[0]
516 516
517 517 @LoginRequired()
518 518 @NotAnonymous()
519 519 @HasRepoPermissionAnyDecorator(
520 520 'repository.read', 'repository.write', 'repository.admin')
521 521 @CSRFRequired()
522 522 def repo_commit_comment_preview(self):
523 523 # Technically a CSRF token is not needed as no state changes with this
524 524 # call. However, as this is a POST is better to have it, so automated
525 525 # tools don't flag it as potential CSRF.
526 526 # Post is required because the payload could be bigger than the maximum
527 527 # allowed by GET.
528 528
529 529 text = self.request.POST.get('text')
530 530 renderer = self.request.POST.get('renderer') or 'rst'
531 531 if text:
532 532 return h.render(text, renderer=renderer, mentions=True,
533 533 repo_name=self.db_repo_name)
534 534 return ''
535 535
536 536 @LoginRequired()
537 537 @HasRepoPermissionAnyDecorator(
538 538 'repository.read', 'repository.write', 'repository.admin')
539 539 @CSRFRequired()
540 540 def repo_commit_comment_history_view(self):
541 541 c = self.load_default_context()
542 542 comment_id = self.request.matchdict['comment_id']
543 543 comment_history_id = self.request.matchdict['comment_history_id']
544 544
545 545 comment = ChangesetComment.get_or_404(comment_id)
546 546 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
547 547 if comment.draft and not comment_owner:
548 548 # if we see draft comments history, we only allow this for owner
549 549 raise HTTPNotFound()
550 550
551 551 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
552 552 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
553 553
554 554 if is_repo_comment:
555 555 c.comment_history = comment_history
556 556
557 557 rendered_comment = render(
558 558 'rhodecode:templates/changeset/comment_history.mako',
559 559 self._get_template_context(c), self.request)
560 560 return rendered_comment
561 561 else:
562 562 log.warning('No permissions for user %s to show comment_history_id: %s',
563 563 self._rhodecode_db_user, comment_history_id)
564 564 raise HTTPNotFound()
565 565
566 566 @LoginRequired()
567 567 @NotAnonymous()
568 568 @HasRepoPermissionAnyDecorator(
569 569 'repository.read', 'repository.write', 'repository.admin')
570 570 @CSRFRequired()
571 571 def repo_commit_comment_attachment_upload(self):
572 572 c = self.load_default_context()
573 573 upload_key = 'attachment'
574 574
575 575 file_obj = self.request.POST.get(upload_key)
576 576
577 577 if file_obj is None:
578 578 self.request.response.status = 400
579 579 return {'store_fid': None,
580 580 'access_path': None,
581 581 'error': f'{upload_key} data field is missing'}
582 582
583 583 if not hasattr(file_obj, 'filename'):
584 584 self.request.response.status = 400
585 585 return {'store_fid': None,
586 586 'access_path': None,
587 587 'error': 'filename cannot be read from the data field'}
588 588
589 589 filename = file_obj.filename
590 590 file_display_name = filename
591 591
592 592 metadata = {
593 593 'user_uploaded': {'username': self._rhodecode_user.username,
594 594 'user_id': self._rhodecode_user.user_id,
595 595 'ip': self._rhodecode_user.ip_addr}}
596 596
597 597 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
598 598 allowed_extensions = [
599 599 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
600 600 '.pptx', '.txt', '.xlsx', '.zip']
601 601 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
602 602
603 603 try:
604 storage = store_utils.get_file_storage(self.request.registry.settings)
605 store_uid, metadata = storage.save_file(
606 file_obj.file, filename, extra_metadata=metadata,
604 f_store = store_utils.get_filestore_backend(self.request.registry.settings)
605 store_uid, metadata = f_store.store(
606 filename, file_obj.file, metadata=metadata,
607 607 extensions=allowed_extensions, max_filesize=max_file_size)
608 608 except FileNotAllowedException:
609 609 self.request.response.status = 400
610 610 permitted_extensions = ', '.join(allowed_extensions)
611 error_msg = 'File `{}` is not allowed. ' \
612 'Only following extensions are permitted: {}'.format(
613 filename, permitted_extensions)
611 error_msg = f'File `{filename}` is not allowed. ' \
612 f'Only following extensions are permitted: {permitted_extensions}'
613
614 614 return {'store_fid': None,
615 615 'access_path': None,
616 616 'error': error_msg}
617 617 except FileOverSizeException:
618 618 self.request.response.status = 400
619 619 limit_mb = h.format_byte_size_binary(max_file_size)
620 error_msg = f'File {filename} is exceeding allowed limit of {limit_mb}.'
620 621 return {'store_fid': None,
621 622 'access_path': None,
622 'error': 'File {} is exceeding allowed limit of {}.'.format(
623 filename, limit_mb)}
623 'error': error_msg}
624 624
625 625 try:
626 626 entry = FileStore.create(
627 627 file_uid=store_uid, filename=metadata["filename"],
628 628 file_hash=metadata["sha256"], file_size=metadata["size"],
629 629 file_display_name=file_display_name,
630 630 file_description=f'comment attachment `{safe_str(filename)}`',
631 631 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
632 632 scope_repo_id=self.db_repo.repo_id
633 633 )
634 634 Session().add(entry)
635 635 Session().commit()
636 636 log.debug('Stored upload in DB as %s', entry)
637 637 except Exception:
638 638 log.exception('Failed to store file %s', filename)
639 639 self.request.response.status = 400
640 640 return {'store_fid': None,
641 641 'access_path': None,
642 642 'error': f'File {filename} failed to store in DB.'}
643 643
644 644 Session().commit()
645 645
646 646 data = {
647 647 'store_fid': store_uid,
648 648 'access_path': h.route_path(
649 649 'download_file', fid=store_uid),
650 650 'fqn_access_path': h.route_url(
651 651 'download_file', fid=store_uid),
652 652 # for EE those are replaced by FQN links on repo-only like
653 653 'repo_access_path': h.route_url(
654 654 'download_file', fid=store_uid),
655 655 'repo_fqn_access_path': h.route_url(
656 656 'download_file', fid=store_uid),
657 657 }
658 658 # this data is a part of CE/EE additional code
659 659 if c.rhodecode_edition_id == 'EE':
660 660 data.update({
661 661 'repo_access_path': h.route_path(
662 662 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
663 663 'repo_fqn_access_path': h.route_url(
664 664 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
665 665 })
666 666
667 667 return data
668 668
669 669 @LoginRequired()
670 670 @NotAnonymous()
671 671 @HasRepoPermissionAnyDecorator(
672 672 'repository.read', 'repository.write', 'repository.admin')
673 673 @CSRFRequired()
674 674 def repo_commit_comment_delete(self):
675 675 commit_id = self.request.matchdict['commit_id']
676 676 comment_id = self.request.matchdict['comment_id']
677 677
678 678 comment = ChangesetComment.get_or_404(comment_id)
679 679 if not comment:
680 680 log.debug('Comment with id:%s not found, skipping', comment_id)
681 681 # comment already deleted in another call probably
682 682 return True
683 683
684 684 if comment.immutable:
685 685 # don't allow deleting comments that are immutable
686 686 raise HTTPForbidden()
687 687
688 688 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
689 689 super_admin = h.HasPermissionAny('hg.admin')()
690 690 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
691 691 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
692 692 comment_repo_admin = is_repo_admin and is_repo_comment
693 693
694 694 if comment.draft and not comment_owner:
695 695 # We never allow to delete draft comments for other than owners
696 696 raise HTTPNotFound()
697 697
698 698 if super_admin or comment_owner or comment_repo_admin:
699 699 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
700 700 Session().commit()
701 701 return True
702 702 else:
703 703 log.warning('No permissions for user %s to delete comment_id: %s',
704 704 self._rhodecode_db_user, comment_id)
705 705 raise HTTPNotFound()
706 706
707 707 @LoginRequired()
708 708 @NotAnonymous()
709 709 @HasRepoPermissionAnyDecorator(
710 710 'repository.read', 'repository.write', 'repository.admin')
711 711 @CSRFRequired()
712 712 def repo_commit_comment_edit(self):
713 713 self.load_default_context()
714 714
715 715 commit_id = self.request.matchdict['commit_id']
716 716 comment_id = self.request.matchdict['comment_id']
717 717 comment = ChangesetComment.get_or_404(comment_id)
718 718
719 719 if comment.immutable:
720 720 # don't allow deleting comments that are immutable
721 721 raise HTTPForbidden()
722 722
723 723 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
724 724 super_admin = h.HasPermissionAny('hg.admin')()
725 725 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
726 726 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
727 727 comment_repo_admin = is_repo_admin and is_repo_comment
728 728
729 729 if super_admin or comment_owner or comment_repo_admin:
730 730 text = self.request.POST.get('text')
731 731 version = self.request.POST.get('version')
732 732 if text == comment.text:
733 733 log.warning(
734 734 'Comment(repo): '
735 735 'Trying to create new version '
736 736 'with the same comment body {}'.format(
737 737 comment_id,
738 738 )
739 739 )
740 740 raise HTTPNotFound()
741 741
742 742 if version.isdigit():
743 743 version = int(version)
744 744 else:
745 745 log.warning(
746 746 'Comment(repo): Wrong version type {} {} '
747 747 'for comment {}'.format(
748 748 version,
749 749 type(version),
750 750 comment_id,
751 751 )
752 752 )
753 753 raise HTTPNotFound()
754 754
755 755 try:
756 756 comment_history = CommentsModel().edit(
757 757 comment_id=comment_id,
758 758 text=text,
759 759 auth_user=self._rhodecode_user,
760 760 version=version,
761 761 )
762 762 except CommentVersionMismatch:
763 763 raise HTTPConflict()
764 764
765 765 if not comment_history:
766 766 raise HTTPNotFound()
767 767
768 768 if not comment.draft:
769 769 commit = self.db_repo.get_commit(commit_id)
770 770 CommentsModel().trigger_commit_comment_hook(
771 771 self.db_repo, self._rhodecode_user, 'edit',
772 772 data={'comment': comment, 'commit': commit})
773 773
774 774 Session().commit()
775 775 return {
776 776 'comment_history_id': comment_history.comment_history_id,
777 777 'comment_id': comment.comment_id,
778 778 'comment_version': comment_history.version,
779 779 'comment_author_username': comment_history.author.username,
780 780 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16, request=self.request),
781 781 'comment_created_on': h.age_component(comment_history.created_on,
782 782 time_is_local=True),
783 783 }
784 784 else:
785 785 log.warning('No permissions for user %s to edit comment_id: %s',
786 786 self._rhodecode_db_user, comment_id)
787 787 raise HTTPNotFound()
788 788
789 789 @LoginRequired()
790 790 @HasRepoPermissionAnyDecorator(
791 791 'repository.read', 'repository.write', 'repository.admin')
792 792 def repo_commit_data(self):
793 793 commit_id = self.request.matchdict['commit_id']
794 794 self.load_default_context()
795 795
796 796 try:
797 797 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
798 798 except CommitDoesNotExistError as e:
799 799 return EmptyCommit(message=str(e))
800 800
801 801 @LoginRequired()
802 802 @HasRepoPermissionAnyDecorator(
803 803 'repository.read', 'repository.write', 'repository.admin')
804 804 def repo_commit_children(self):
805 805 commit_id = self.request.matchdict['commit_id']
806 806 self.load_default_context()
807 807
808 808 try:
809 809 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
810 810 children = commit.children
811 811 except CommitDoesNotExistError:
812 812 children = []
813 813
814 814 result = {"results": children}
815 815 return result
816 816
817 817 @LoginRequired()
818 818 @HasRepoPermissionAnyDecorator(
819 819 'repository.read', 'repository.write', 'repository.admin')
820 820 def repo_commit_parents(self):
821 821 commit_id = self.request.matchdict['commit_id']
822 822 self.load_default_context()
823 823
824 824 try:
825 825 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
826 826 parents = commit.parents
827 827 except CommitDoesNotExistError:
828 828 parents = []
829 829 result = {"results": parents}
830 830 return result
@@ -1,1716 +1,1716 b''
1 1 # Copyright (C) 2011-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import itertools
20 20 import logging
21 21 import os
22 22 import collections
23 23 import urllib.request
24 24 import urllib.parse
25 25 import urllib.error
26 26 import pathlib
27 27 import time
28 28 import random
29 29
30 30 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
31 31
32 32 from pyramid.renderers import render
33 33 from pyramid.response import Response
34 34
35 35 import rhodecode
36 36 from rhodecode.apps._base import RepoAppView
37 37
38 38
39 39 from rhodecode.lib import diffs, helpers as h, rc_cache
40 40 from rhodecode.lib import audit_logger
41 41 from rhodecode.lib.hash_utils import sha1_safe
42 42 from rhodecode.lib.archive_cache import (
43 43 get_archival_cache_store, get_archival_config, ArchiveCacheGenerationLock, archive_iterator)
44 44 from rhodecode.lib.str_utils import safe_bytes, convert_special_chars
45 45 from rhodecode.lib.view_utils import parse_path_ref
46 46 from rhodecode.lib.exceptions import NonRelativePathError
47 47 from rhodecode.lib.codeblocks import (
48 48 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
49 49 from rhodecode.lib.utils2 import convert_line_endings, detect_mode
50 50 from rhodecode.lib.type_utils import str2bool
51 from rhodecode.lib.str_utils import safe_str, safe_int
51 from rhodecode.lib.str_utils import safe_str, safe_int, header_safe_str
52 52 from rhodecode.lib.auth import (
53 53 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
54 54 from rhodecode.lib.vcs import path as vcspath
55 55 from rhodecode.lib.vcs.backends.base import EmptyCommit
56 56 from rhodecode.lib.vcs.conf import settings
57 57 from rhodecode.lib.vcs.nodes import FileNode
58 58 from rhodecode.lib.vcs.exceptions import (
59 59 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
60 60 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
61 61 NodeDoesNotExistError, CommitError, NodeError)
62 62
63 63 from rhodecode.model.scm import ScmModel
64 64 from rhodecode.model.db import Repository
65 65
66 66 log = logging.getLogger(__name__)
67 67
68 68
69 69 def get_archive_name(db_repo_id, db_repo_name, commit_sha, ext, subrepos=False, path_sha='', with_hash=True):
70 70 # original backward compat name of archive
71 71 clean_name = safe_str(convert_special_chars(db_repo_name).replace('/', '_'))
72 72
73 73 # e.g vcsserver-id-abcd-sub-1-abcfdef-archive-all.zip
74 74 # vcsserver-id-abcd-sub-0-abcfdef-COMMIT_SHA-PATH_SHA.zip
75 75 id_sha = sha1_safe(str(db_repo_id))[:4]
76 76 sub_repo = 'sub-1' if subrepos else 'sub-0'
77 77 commit = commit_sha if with_hash else 'archive'
78 78 path_marker = (path_sha if with_hash else '') or 'all'
79 79 archive_name = f'{clean_name}-id-{id_sha}-{sub_repo}-{commit}-{path_marker}{ext}'
80 80
81 81 return archive_name
82 82
83 83
84 84 def get_path_sha(at_path):
85 85 return safe_str(sha1_safe(at_path)[:8])
86 86
87 87
88 88 def _get_archive_spec(fname):
89 89 log.debug('Detecting archive spec for: `%s`', fname)
90 90
91 91 fileformat = None
92 92 ext = None
93 93 content_type = None
94 94 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
95 95
96 96 if fname.endswith(extension):
97 97 fileformat = a_type
98 98 log.debug('archive is of type: %s', fileformat)
99 99 ext = extension
100 100 break
101 101
102 102 if not fileformat:
103 103 raise ValueError()
104 104
105 105 # left over part of whole fname is the commit
106 106 commit_id = fname[:-len(ext)]
107 107
108 108 return commit_id, ext, fileformat, content_type
109 109
110 110
111 111 class RepoFilesView(RepoAppView):
112 112
113 113 @staticmethod
114 114 def adjust_file_path_for_svn(f_path, repo):
115 115 """
116 116 Computes the relative path of `f_path`.
117 117
118 118 This is mainly based on prefix matching of the recognized tags and
119 119 branches in the underlying repository.
120 120 """
121 121 tags_and_branches = itertools.chain(
122 122 repo.branches.keys(),
123 123 repo.tags.keys())
124 124 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
125 125
126 126 for name in tags_and_branches:
127 127 if f_path.startswith(f'{name}/'):
128 128 f_path = vcspath.relpath(f_path, name)
129 129 break
130 130 return f_path
131 131
132 132 def load_default_context(self):
133 133 c = self._get_local_tmpl_context(include_app_defaults=True)
134 134 c.rhodecode_repo = self.rhodecode_vcs_repo
135 135 c.enable_downloads = self.db_repo.enable_downloads
136 136 return c
137 137
138 138 def _ensure_not_locked(self, commit_id='tip'):
139 139 _ = self.request.translate
140 140
141 141 repo = self.db_repo
142 142 if repo.enable_locking and repo.locked[0]:
143 143 h.flash(_('This repository has been locked by %s on %s')
144 144 % (h.person_by_id(repo.locked[0]),
145 145 h.format_date(h.time_to_datetime(repo.locked[1]))),
146 146 'warning')
147 147 files_url = h.route_path(
148 148 'repo_files:default_path',
149 149 repo_name=self.db_repo_name, commit_id=commit_id)
150 150 raise HTTPFound(files_url)
151 151
152 152 def forbid_non_head(self, is_head, f_path, commit_id='tip', json_mode=False):
153 153 _ = self.request.translate
154 154
155 155 if not is_head:
156 156 message = _('Cannot modify file. '
157 157 'Given commit `{}` is not head of a branch.').format(commit_id)
158 158 h.flash(message, category='warning')
159 159
160 160 if json_mode:
161 161 return message
162 162
163 163 files_url = h.route_path(
164 164 'repo_files', repo_name=self.db_repo_name, commit_id=commit_id,
165 165 f_path=f_path)
166 166 raise HTTPFound(files_url)
167 167
168 168 def check_branch_permission(self, branch_name, commit_id='tip', json_mode=False):
169 169 _ = self.request.translate
170 170
171 171 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
172 172 self.db_repo_name, branch_name)
173 173 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
174 174 message = _('Branch `{}` changes forbidden by rule {}.').format(
175 175 h.escape(branch_name), h.escape(rule))
176 176 h.flash(message, 'warning')
177 177
178 178 if json_mode:
179 179 return message
180 180
181 181 files_url = h.route_path(
182 182 'repo_files:default_path', repo_name=self.db_repo_name, commit_id=commit_id)
183 183
184 184 raise HTTPFound(files_url)
185 185
186 186 def _get_commit_and_path(self):
187 187 default_commit_id = self.db_repo.landing_ref_name
188 188 default_f_path = '/'
189 189
190 190 commit_id = self.request.matchdict.get(
191 191 'commit_id', default_commit_id)
192 192 f_path = self._get_f_path(self.request.matchdict, default_f_path)
193 193 return commit_id, f_path
194 194
195 195 def _get_default_encoding(self, c):
196 196 enc_list = getattr(c, 'default_encodings', [])
197 197 return enc_list[0] if enc_list else 'UTF-8'
198 198
199 199 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
200 200 """
201 201 This is a safe way to get commit. If an error occurs it redirects to
202 202 tip with proper message
203 203
204 204 :param commit_id: id of commit to fetch
205 205 :param redirect_after: toggle redirection
206 206 """
207 207 _ = self.request.translate
208 208
209 209 try:
210 210 return self.rhodecode_vcs_repo.get_commit(commit_id)
211 211 except EmptyRepositoryError:
212 212 if not redirect_after:
213 213 return None
214 214
215 215 add_new = upload_new = ""
216 216 if h.HasRepoPermissionAny(
217 217 'repository.write', 'repository.admin')(self.db_repo_name):
218 218 _url = h.route_path(
219 219 'repo_files_add_file',
220 220 repo_name=self.db_repo_name, commit_id=0, f_path='')
221 221 add_new = h.link_to(
222 222 _('add a new file'), _url, class_="alert-link")
223 223
224 224 _url_upld = h.route_path(
225 225 'repo_files_upload_file',
226 226 repo_name=self.db_repo_name, commit_id=0, f_path='')
227 227 upload_new = h.link_to(
228 228 _('upload a new file'), _url_upld, class_="alert-link")
229 229
230 230 h.flash(h.literal(
231 231 _('There are no files yet. Click here to %s or %s.') % (add_new, upload_new)), category='warning')
232 232 raise HTTPFound(
233 233 h.route_path('repo_summary', repo_name=self.db_repo_name))
234 234
235 235 except (CommitDoesNotExistError, LookupError) as e:
236 236 msg = _('No such commit exists for this repository. Commit: {}').format(commit_id)
237 237 h.flash(msg, category='error')
238 238 raise HTTPNotFound()
239 239 except RepositoryError as e:
240 240 h.flash(h.escape(safe_str(e)), category='error')
241 241 raise HTTPNotFound()
242 242
243 243 def _get_filenode_or_redirect(self, commit_obj, path, pre_load=None):
244 244 """
245 245 Returns file_node, if error occurs or given path is directory,
246 246 it'll redirect to top level path
247 247 """
248 248 _ = self.request.translate
249 249
250 250 try:
251 251 file_node = commit_obj.get_node(path, pre_load=pre_load)
252 252 if file_node.is_dir():
253 253 raise RepositoryError('The given path is a directory')
254 254 except CommitDoesNotExistError:
255 255 log.exception('No such commit exists for this repository')
256 256 h.flash(_('No such commit exists for this repository'), category='error')
257 257 raise HTTPNotFound()
258 258 except RepositoryError as e:
259 259 log.warning('Repository error while fetching filenode `%s`. Err:%s', path, e)
260 260 h.flash(h.escape(safe_str(e)), category='error')
261 261 raise HTTPNotFound()
262 262
263 263 return file_node
264 264
265 265 def _is_valid_head(self, commit_id, repo, landing_ref):
266 266 branch_name = sha_commit_id = ''
267 267 is_head = False
268 268 log.debug('Checking if commit_id `%s` is a head for %s.', commit_id, repo)
269 269
270 270 for _branch_name, branch_commit_id in repo.branches.items():
271 271 # simple case we pass in branch name, it's a HEAD
272 272 if commit_id == _branch_name:
273 273 is_head = True
274 274 branch_name = _branch_name
275 275 sha_commit_id = branch_commit_id
276 276 break
277 277 # case when we pass in full sha commit_id, which is a head
278 278 elif commit_id == branch_commit_id:
279 279 is_head = True
280 280 branch_name = _branch_name
281 281 sha_commit_id = branch_commit_id
282 282 break
283 283
284 284 if h.is_svn(repo) and not repo.is_empty():
285 285 # Note: Subversion only has one head.
286 286 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
287 287 is_head = True
288 288 return branch_name, sha_commit_id, is_head
289 289
290 290 # checked branches, means we only need to try to get the branch/commit_sha
291 291 if repo.is_empty():
292 292 is_head = True
293 293 branch_name = landing_ref
294 294 sha_commit_id = EmptyCommit().raw_id
295 295 else:
296 296 commit = repo.get_commit(commit_id=commit_id)
297 297 if commit:
298 298 branch_name = commit.branch
299 299 sha_commit_id = commit.raw_id
300 300
301 301 return branch_name, sha_commit_id, is_head
302 302
303 303 def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False, at_rev=None):
304 304
305 305 repo_id = self.db_repo.repo_id
306 306 force_recache = self.get_recache_flag()
307 307
308 308 cache_seconds = safe_int(
309 309 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
310 310 cache_on = not force_recache and cache_seconds > 0
311 311 log.debug(
312 312 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
313 313 'with caching: %s[TTL: %ss]' % (
314 314 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
315 315
316 316 cache_namespace_uid = f'repo.{rc_cache.FILE_TREE_CACHE_VER}.{repo_id}'
317 317 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
318 318
319 319 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
320 320 def compute_file_tree(_name_hash, _repo_id, _commit_id, _f_path, _full_load, _at_rev):
321 321 log.debug('Generating cached file tree at for repo_id: %s, %s, %s',
322 322 _repo_id, _commit_id, _f_path)
323 323
324 324 c.full_load = _full_load
325 325 return render(
326 326 'rhodecode:templates/files/files_browser_tree.mako',
327 327 self._get_template_context(c), self.request, _at_rev)
328 328
329 329 return compute_file_tree(
330 330 self.db_repo.repo_name_hash, self.db_repo.repo_id, commit_id, f_path, full_load, at_rev)
331 331
332 332 def create_pure_path(self, *parts):
333 333 # Split paths and sanitize them, removing any ../ etc
334 334 sanitized_path = [
335 335 x for x in pathlib.PurePath(*parts).parts
336 336 if x not in ['.', '..']]
337 337
338 338 pure_path = pathlib.PurePath(*sanitized_path)
339 339 return pure_path
340 340
341 341 def _is_lf_enabled(self, target_repo):
342 342 lf_enabled = False
343 343
344 344 lf_key_for_vcs_map = {
345 345 'hg': 'extensions_largefiles',
346 346 'git': 'vcs_git_lfs_enabled'
347 347 }
348 348
349 349 lf_key_for_vcs = lf_key_for_vcs_map.get(target_repo.repo_type)
350 350
351 351 if lf_key_for_vcs:
352 352 lf_enabled = self._get_repo_setting(target_repo, lf_key_for_vcs)
353 353
354 354 return lf_enabled
355 355
356 356 @LoginRequired()
357 357 @HasRepoPermissionAnyDecorator(
358 358 'repository.read', 'repository.write', 'repository.admin')
359 359 def repo_archivefile(self):
360 360 # archive cache config
361 361 from rhodecode import CONFIG
362 362 _ = self.request.translate
363 363 self.load_default_context()
364 364 default_at_path = '/'
365 365 fname = self.request.matchdict['fname']
366 366 subrepos = self.request.GET.get('subrepos') == 'true'
367 367 with_hash = str2bool(self.request.GET.get('with_hash', '1'))
368 368 at_path = self.request.GET.get('at_path') or default_at_path
369 369
370 370 if not self.db_repo.enable_downloads:
371 371 return Response(_('Downloads disabled'))
372 372
373 373 try:
374 374 commit_id, ext, fileformat, content_type = \
375 375 _get_archive_spec(fname)
376 376 except ValueError:
377 377 return Response(_('Unknown archive type for: `{}`').format(
378 378 h.escape(fname)))
379 379
380 380 try:
381 381 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
382 382 except CommitDoesNotExistError:
383 383 return Response(_('Unknown commit_id {}').format(
384 384 h.escape(commit_id)))
385 385 except EmptyRepositoryError:
386 386 return Response(_('Empty repository'))
387 387
388 388 # we used a ref, or a shorter version, lets redirect client ot use explicit hash
389 389 if commit_id != commit.raw_id:
390 390 fname=f'{commit.raw_id}{ext}'
391 391 raise HTTPFound(self.request.current_route_path(fname=fname))
392 392
393 393 try:
394 394 at_path = commit.get_node(at_path).path or default_at_path
395 395 except Exception:
396 396 return Response(_('No node at path {} for this repository').format(h.escape(at_path)))
397 397
398 398 path_sha = get_path_sha(at_path)
399 399
400 400 # used for cache etc, consistent unique archive name
401 401 archive_name_key = get_archive_name(
402 402 self.db_repo.repo_id, self.db_repo_name, commit_sha=commit.short_id, ext=ext, subrepos=subrepos,
403 403 path_sha=path_sha, with_hash=True)
404 404
405 405 if not with_hash:
406 406 path_sha = ''
407 407
408 408 # what end client gets served
409 409 response_archive_name = get_archive_name(
410 410 self.db_repo.repo_id, self.db_repo_name, commit_sha=commit.short_id, ext=ext, subrepos=subrepos,
411 411 path_sha=path_sha, with_hash=with_hash)
412 412
413 413 # remove extension from our archive directory name
414 414 archive_dir_name = response_archive_name[:-len(ext)]
415 415
416 416 archive_cache_disable = self.request.GET.get('no_cache')
417 417
418 418 d_cache = get_archival_cache_store(config=CONFIG)
419 419
420 420 # NOTE: we get the config to pass to a call to lazy-init the SAME type of cache on vcsserver
421 421 d_cache_conf = get_archival_config(config=CONFIG)
422 422
423 423 # This is also a cache key, and lock key
424 424 reentrant_lock_key = archive_name_key + '.lock'
425 425
426 426 use_cached_archive = False
427 427 if not archive_cache_disable and archive_name_key in d_cache:
428 428 reader, metadata = d_cache.fetch(archive_name_key)
429 429
430 430 use_cached_archive = True
431 431 log.debug('Found cached archive as key=%s tag=%s, serving archive from cache reader=%s',
432 432 archive_name_key, metadata, reader.name)
433 433 else:
434 434 reader = None
435 435 log.debug('Archive with key=%s is not yet cached, creating one now...', archive_name_key)
436 436
437 437 if not reader:
438 438 # generate new archive, as previous was not found in the cache
439 439 try:
440 440 with d_cache.get_lock(reentrant_lock_key):
441 441 try:
442 442 commit.archive_repo(archive_name_key, archive_dir_name=archive_dir_name,
443 443 kind=fileformat, subrepos=subrepos,
444 444 archive_at_path=at_path, cache_config=d_cache_conf)
445 445 except ImproperArchiveTypeError:
446 446 return _('Unknown archive type')
447 447
448 448 except ArchiveCacheGenerationLock:
449 449 retry_after = round(random.uniform(0.3, 3.0), 1)
450 450 time.sleep(retry_after)
451 451
452 452 location = self.request.url
453 453 response = Response(
454 454 f"archive {archive_name_key} generation in progress, Retry-After={retry_after}, Location={location}"
455 455 )
456 456 response.headers["Retry-After"] = str(retry_after)
457 457 response.status_code = 307 # temporary redirect
458 458
459 459 response.location = location
460 460 return response
461 461
462 462 reader, metadata = d_cache.fetch(archive_name_key, retry=True, retry_attempts=30)
463 463
464 464 response = Response(app_iter=archive_iterator(reader))
465 465 response.content_disposition = f'attachment; filename={response_archive_name}'
466 466 response.content_type = str(content_type)
467 467
468 468 try:
469 469 return response
470 470 finally:
471 471 # store download action
472 472 audit_logger.store_web(
473 473 'repo.archive.download', action_data={
474 474 'user_agent': self.request.user_agent,
475 475 'archive_name': archive_name_key,
476 476 'archive_spec': fname,
477 477 'archive_cached': use_cached_archive},
478 478 user=self._rhodecode_user,
479 479 repo=self.db_repo,
480 480 commit=True
481 481 )
482 482
483 483 def _get_file_node(self, commit_id, f_path):
484 484 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
485 485 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
486 486 try:
487 487 node = commit.get_node(f_path)
488 488 if node.is_dir():
489 489 raise NodeError(f'{node} path is a {type(node)} not a file')
490 490 except NodeDoesNotExistError:
491 491 commit = EmptyCommit(
492 492 commit_id=commit_id,
493 493 idx=commit.idx,
494 494 repo=commit.repository,
495 495 alias=commit.repository.alias,
496 496 message=commit.message,
497 497 author=commit.author,
498 498 date=commit.date)
499 499 node = FileNode(safe_bytes(f_path), b'', commit=commit)
500 500 else:
501 501 commit = EmptyCommit(
502 502 repo=self.rhodecode_vcs_repo,
503 503 alias=self.rhodecode_vcs_repo.alias)
504 504 node = FileNode(safe_bytes(f_path), b'', commit=commit)
505 505 return node
506 506
507 507 @LoginRequired()
508 508 @HasRepoPermissionAnyDecorator(
509 509 'repository.read', 'repository.write', 'repository.admin')
510 510 def repo_files_diff(self):
511 511 c = self.load_default_context()
512 512 f_path = self._get_f_path(self.request.matchdict)
513 513 diff1 = self.request.GET.get('diff1', '')
514 514 diff2 = self.request.GET.get('diff2', '')
515 515
516 516 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
517 517
518 518 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
519 519 line_context = self.request.GET.get('context', 3)
520 520
521 521 if not any((diff1, diff2)):
522 522 h.flash(
523 523 'Need query parameter "diff1" or "diff2" to generate a diff.',
524 524 category='error')
525 525 raise HTTPBadRequest()
526 526
527 527 c.action = self.request.GET.get('diff')
528 528 if c.action not in ['download', 'raw']:
529 529 compare_url = h.route_path(
530 530 'repo_compare',
531 531 repo_name=self.db_repo_name,
532 532 source_ref_type='rev',
533 533 source_ref=diff1,
534 534 target_repo=self.db_repo_name,
535 535 target_ref_type='rev',
536 536 target_ref=diff2,
537 537 _query=dict(f_path=f_path))
538 538 # redirect to new view if we render diff
539 539 raise HTTPFound(compare_url)
540 540
541 541 try:
542 542 node1 = self._get_file_node(diff1, path1)
543 543 node2 = self._get_file_node(diff2, f_path)
544 544 except (RepositoryError, NodeError):
545 545 log.exception("Exception while trying to get node from repository")
546 546 raise HTTPFound(
547 547 h.route_path('repo_files', repo_name=self.db_repo_name,
548 548 commit_id='tip', f_path=f_path))
549 549
550 550 if all(isinstance(node.commit, EmptyCommit)
551 551 for node in (node1, node2)):
552 552 raise HTTPNotFound()
553 553
554 554 c.commit_1 = node1.commit
555 555 c.commit_2 = node2.commit
556 556
557 557 if c.action == 'download':
558 558 _diff = diffs.get_gitdiff(node1, node2,
559 559 ignore_whitespace=ignore_whitespace,
560 560 context=line_context)
561 561 # NOTE: this was using diff_format='gitdiff'
562 562 diff = diffs.DiffProcessor(_diff, diff_format='newdiff')
563 563
564 564 response = Response(self.path_filter.get_raw_patch(diff))
565 565 response.content_type = 'text/plain'
566 566 response.content_disposition = (
567 567 f'attachment; filename={f_path}_{diff1}_vs_{diff2}.diff'
568 568 )
569 569 charset = self._get_default_encoding(c)
570 570 if charset:
571 571 response.charset = charset
572 572 return response
573 573
574 574 elif c.action == 'raw':
575 575 _diff = diffs.get_gitdiff(node1, node2,
576 576 ignore_whitespace=ignore_whitespace,
577 577 context=line_context)
578 578 # NOTE: this was using diff_format='gitdiff'
579 579 diff = diffs.DiffProcessor(_diff, diff_format='newdiff')
580 580
581 581 response = Response(self.path_filter.get_raw_patch(diff))
582 582 response.content_type = 'text/plain'
583 583 charset = self._get_default_encoding(c)
584 584 if charset:
585 585 response.charset = charset
586 586 return response
587 587
588 588 # in case we ever end up here
589 589 raise HTTPNotFound()
590 590
591 591 @LoginRequired()
592 592 @HasRepoPermissionAnyDecorator(
593 593 'repository.read', 'repository.write', 'repository.admin')
594 594 def repo_files_diff_2way_redirect(self):
595 595 """
596 596 Kept only to make OLD links work
597 597 """
598 598 f_path = self._get_f_path_unchecked(self.request.matchdict)
599 599 diff1 = self.request.GET.get('diff1', '')
600 600 diff2 = self.request.GET.get('diff2', '')
601 601
602 602 if not any((diff1, diff2)):
603 603 h.flash(
604 604 'Need query parameter "diff1" or "diff2" to generate a diff.',
605 605 category='error')
606 606 raise HTTPBadRequest()
607 607
608 608 compare_url = h.route_path(
609 609 'repo_compare',
610 610 repo_name=self.db_repo_name,
611 611 source_ref_type='rev',
612 612 source_ref=diff1,
613 613 target_ref_type='rev',
614 614 target_ref=diff2,
615 615 _query=dict(f_path=f_path, diffmode='sideside',
616 616 target_repo=self.db_repo_name,))
617 617 raise HTTPFound(compare_url)
618 618
619 619 @LoginRequired()
620 620 def repo_files_default_commit_redirect(self):
621 621 """
622 622 Special page that redirects to the landing page of files based on the default
623 623 commit for repository
624 624 """
625 625 c = self.load_default_context()
626 626 ref_name = c.rhodecode_db_repo.landing_ref_name
627 627 landing_url = h.repo_files_by_ref_url(
628 628 c.rhodecode_db_repo.repo_name,
629 629 c.rhodecode_db_repo.repo_type,
630 630 f_path='',
631 631 ref_name=ref_name,
632 632 commit_id='tip',
633 633 query=dict(at=ref_name)
634 634 )
635 635
636 636 raise HTTPFound(landing_url)
637 637
638 638 @LoginRequired()
639 639 @HasRepoPermissionAnyDecorator(
640 640 'repository.read', 'repository.write', 'repository.admin')
641 641 def repo_files(self):
642 642 c = self.load_default_context()
643 643
644 644 view_name = getattr(self.request.matched_route, 'name', None)
645 645
646 646 c.annotate = view_name == 'repo_files:annotated'
647 647 # default is false, but .rst/.md files later are auto rendered, we can
648 648 # overwrite auto rendering by setting this GET flag
649 649 c.renderer = view_name == 'repo_files:rendered' or not self.request.GET.get('no-render', False)
650 650
651 651 commit_id, f_path = self._get_commit_and_path()
652 652
653 653 c.commit = self._get_commit_or_redirect(commit_id)
654 654 c.branch = self.request.GET.get('branch', None)
655 655 c.f_path = f_path
656 656 at_rev = self.request.GET.get('at')
657 657
658 658 # files or dirs
659 659 try:
660 660 c.file = c.commit.get_node(f_path, pre_load=['is_binary', 'size', 'data'])
661 661
662 662 c.file_author = True
663 663 c.file_tree = ''
664 664
665 665 # prev link
666 666 try:
667 667 prev_commit = c.commit.prev(c.branch)
668 668 c.prev_commit = prev_commit
669 669 c.url_prev = h.route_path(
670 670 'repo_files', repo_name=self.db_repo_name,
671 671 commit_id=prev_commit.raw_id, f_path=f_path)
672 672 if c.branch:
673 673 c.url_prev += '?branch=%s' % c.branch
674 674 except (CommitDoesNotExistError, VCSError):
675 675 c.url_prev = '#'
676 676 c.prev_commit = EmptyCommit()
677 677
678 678 # next link
679 679 try:
680 680 next_commit = c.commit.next(c.branch)
681 681 c.next_commit = next_commit
682 682 c.url_next = h.route_path(
683 683 'repo_files', repo_name=self.db_repo_name,
684 684 commit_id=next_commit.raw_id, f_path=f_path)
685 685 if c.branch:
686 686 c.url_next += '?branch=%s' % c.branch
687 687 except (CommitDoesNotExistError, VCSError):
688 688 c.url_next = '#'
689 689 c.next_commit = EmptyCommit()
690 690
691 691 # load file content
692 692 if c.file.is_file():
693 693
694 694 c.lf_node = {}
695 695
696 696 has_lf_enabled = self._is_lf_enabled(self.db_repo)
697 697 if has_lf_enabled:
698 698 c.lf_node = c.file.get_largefile_node()
699 699
700 700 c.file_source_page = 'true'
701 701 c.file_last_commit = c.file.last_commit
702 702
703 703 c.file_size_too_big = c.file.size > c.visual.cut_off_limit_file
704 704
705 705 if not (c.file_size_too_big or c.file.is_binary):
706 706 if c.annotate: # annotation has precedence over renderer
707 707 c.annotated_lines = filenode_as_annotated_lines_tokens(
708 708 c.file
709 709 )
710 710 else:
711 711 c.renderer = (
712 712 c.renderer and h.renderer_from_filename(c.file.path)
713 713 )
714 714 if not c.renderer:
715 715 c.lines = filenode_as_lines_tokens(c.file)
716 716
717 717 _branch_name, _sha_commit_id, is_head = \
718 718 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
719 719 landing_ref=self.db_repo.landing_ref_name)
720 720 c.on_branch_head = is_head
721 721
722 722 branch = c.commit.branch if (
723 723 c.commit.branch and '/' not in c.commit.branch) else None
724 724 c.branch_or_raw_id = branch or c.commit.raw_id
725 725 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
726 726
727 727 author = c.file_last_commit.author
728 728 c.authors = [[
729 729 h.email(author),
730 730 h.person(author, 'username_or_name_or_email'),
731 731 1
732 732 ]]
733 733
734 734 else: # load tree content at path
735 735 c.file_source_page = 'false'
736 736 c.authors = []
737 737 # this loads a simple tree without metadata to speed things up
738 738 # later via ajax we call repo_nodetree_full and fetch whole
739 739 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path, at_rev=at_rev)
740 740
741 741 c.readme_data, c.readme_file = \
742 742 self._get_readme_data(self.db_repo, c.visual.default_renderer,
743 743 c.commit.raw_id, f_path)
744 744
745 745 except RepositoryError as e:
746 746 h.flash(h.escape(safe_str(e)), category='error')
747 747 raise HTTPNotFound()
748 748
749 749 if self.request.environ.get('HTTP_X_PJAX'):
750 750 html = render('rhodecode:templates/files/files_pjax.mako',
751 751 self._get_template_context(c), self.request)
752 752 else:
753 753 html = render('rhodecode:templates/files/files.mako',
754 754 self._get_template_context(c), self.request)
755 755 return Response(html)
756 756
757 757 @HasRepoPermissionAnyDecorator(
758 758 'repository.read', 'repository.write', 'repository.admin')
759 759 def repo_files_annotated_previous(self):
760 760 self.load_default_context()
761 761
762 762 commit_id, f_path = self._get_commit_and_path()
763 763 commit = self._get_commit_or_redirect(commit_id)
764 764 prev_commit_id = commit.raw_id
765 765 line_anchor = self.request.GET.get('line_anchor')
766 766 is_file = False
767 767 try:
768 768 _file = commit.get_node(f_path)
769 769 is_file = _file.is_file()
770 770 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
771 771 pass
772 772
773 773 if is_file:
774 774 history = commit.get_path_history(f_path)
775 775 prev_commit_id = history[1].raw_id \
776 776 if len(history) > 1 else prev_commit_id
777 777 prev_url = h.route_path(
778 778 'repo_files:annotated', repo_name=self.db_repo_name,
779 779 commit_id=prev_commit_id, f_path=f_path,
780 780 _anchor=f'L{line_anchor}')
781 781
782 782 raise HTTPFound(prev_url)
783 783
784 784 @LoginRequired()
785 785 @HasRepoPermissionAnyDecorator(
786 786 'repository.read', 'repository.write', 'repository.admin')
787 787 def repo_nodetree_full(self):
788 788 """
789 789 Returns rendered html of file tree that contains commit date,
790 790 author, commit_id for the specified combination of
791 791 repo, commit_id and file path
792 792 """
793 793 c = self.load_default_context()
794 794
795 795 commit_id, f_path = self._get_commit_and_path()
796 796 commit = self._get_commit_or_redirect(commit_id)
797 797 try:
798 798 dir_node = commit.get_node(f_path)
799 799 except RepositoryError as e:
800 800 return Response(f'error: {h.escape(safe_str(e))}')
801 801
802 802 if dir_node.is_file():
803 803 return Response('')
804 804
805 805 c.file = dir_node
806 806 c.commit = commit
807 807 at_rev = self.request.GET.get('at')
808 808
809 809 html = self._get_tree_at_commit(
810 810 c, commit.raw_id, dir_node.path, full_load=True, at_rev=at_rev)
811 811
812 812 return Response(html)
813 813
814 814 def _get_attachement_headers(self, f_path):
815 815 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
816 816 safe_path = f_name.replace('"', '\\"')
817 817 encoded_path = urllib.parse.quote(f_name)
818 818
819 819 headers = "attachment; " \
820 820 "filename=\"{}\"; " \
821 821 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
822 822
823 return safe_bytes(headers).decode('latin-1', errors='replace')
823 return header_safe_str(headers)
824 824
825 825 @LoginRequired()
826 826 @HasRepoPermissionAnyDecorator(
827 827 'repository.read', 'repository.write', 'repository.admin')
828 828 def repo_file_raw(self):
829 829 """
830 830 Action for show as raw, some mimetypes are "rendered",
831 831 those include images, icons.
832 832 """
833 833 c = self.load_default_context()
834 834
835 835 commit_id, f_path = self._get_commit_and_path()
836 836 commit = self._get_commit_or_redirect(commit_id)
837 837 file_node = self._get_filenode_or_redirect(commit, f_path)
838 838
839 839 raw_mimetype_mapping = {
840 840 # map original mimetype to a mimetype used for "show as raw"
841 841 # you can also provide a content-disposition to override the
842 842 # default "attachment" disposition.
843 843 # orig_type: (new_type, new_dispo)
844 844
845 845 # show images inline:
846 846 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
847 847 # for example render an SVG with javascript inside or even render
848 848 # HTML.
849 849 'image/x-icon': ('image/x-icon', 'inline'),
850 850 'image/png': ('image/png', 'inline'),
851 851 'image/gif': ('image/gif', 'inline'),
852 852 'image/jpeg': ('image/jpeg', 'inline'),
853 853 'application/pdf': ('application/pdf', 'inline'),
854 854 }
855 855
856 856 mimetype = file_node.mimetype
857 857 try:
858 858 mimetype, disposition = raw_mimetype_mapping[mimetype]
859 859 except KeyError:
860 860 # we don't know anything special about this, handle it safely
861 861 if file_node.is_binary:
862 862 # do same as download raw for binary files
863 863 mimetype, disposition = 'application/octet-stream', 'attachment'
864 864 else:
865 865 # do not just use the original mimetype, but force text/plain,
866 866 # otherwise it would serve text/html and that might be unsafe.
867 867 # Note: underlying vcs library fakes text/plain mimetype if the
868 868 # mimetype can not be determined and it thinks it is not
869 869 # binary.This might lead to erroneous text display in some
870 870 # cases, but helps in other cases, like with text files
871 871 # without extension.
872 872 mimetype, disposition = 'text/plain', 'inline'
873 873
874 874 if disposition == 'attachment':
875 875 disposition = self._get_attachement_headers(f_path)
876 876
877 877 stream_content = file_node.stream_bytes()
878 878
879 879 response = Response(app_iter=stream_content)
880 880 response.content_disposition = disposition
881 881 response.content_type = mimetype
882 882
883 883 charset = self._get_default_encoding(c)
884 884 if charset:
885 885 response.charset = charset
886 886
887 887 return response
888 888
889 889 @LoginRequired()
890 890 @HasRepoPermissionAnyDecorator(
891 891 'repository.read', 'repository.write', 'repository.admin')
892 892 def repo_file_download(self):
893 893 c = self.load_default_context()
894 894
895 895 commit_id, f_path = self._get_commit_and_path()
896 896 commit = self._get_commit_or_redirect(commit_id)
897 897 file_node = self._get_filenode_or_redirect(commit, f_path)
898 898
899 899 if self.request.GET.get('lf'):
900 900 # only if lf get flag is passed, we download this file
901 901 # as LFS/Largefile
902 902 lf_node = file_node.get_largefile_node()
903 903 if lf_node:
904 904 # overwrite our pointer with the REAL large-file
905 905 file_node = lf_node
906 906
907 907 disposition = self._get_attachement_headers(f_path)
908 908
909 909 stream_content = file_node.stream_bytes()
910 910
911 911 response = Response(app_iter=stream_content)
912 912 response.content_disposition = disposition
913 913 response.content_type = file_node.mimetype
914 914
915 915 charset = self._get_default_encoding(c)
916 916 if charset:
917 917 response.charset = charset
918 918
919 919 return response
920 920
921 921 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
922 922
923 923 cache_seconds = safe_int(
924 924 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
925 925 cache_on = cache_seconds > 0
926 926 log.debug(
927 927 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
928 928 'with caching: %s[TTL: %ss]' % (
929 929 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
930 930
931 931 cache_namespace_uid = f'repo.{repo_id}'
932 932 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
933 933
934 934 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
935 935 def compute_file_search(_name_hash, _repo_id, _commit_id, _f_path):
936 936 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
937 937 _repo_id, commit_id, f_path)
938 938 try:
939 939 _d, _f = ScmModel().get_quick_filter_nodes(repo_name, _commit_id, _f_path)
940 940 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
941 941 log.exception(safe_str(e))
942 942 h.flash(h.escape(safe_str(e)), category='error')
943 943 raise HTTPFound(h.route_path(
944 944 'repo_files', repo_name=self.db_repo_name,
945 945 commit_id='tip', f_path='/'))
946 946
947 947 return _d + _f
948 948
949 949 result = compute_file_search(self.db_repo.repo_name_hash, self.db_repo.repo_id,
950 950 commit_id, f_path)
951 951 return filter(lambda n: self.path_filter.path_access_allowed(n['name']), result)
952 952
953 953 @LoginRequired()
954 954 @HasRepoPermissionAnyDecorator(
955 955 'repository.read', 'repository.write', 'repository.admin')
956 956 def repo_nodelist(self):
957 957 self.load_default_context()
958 958
959 959 commit_id, f_path = self._get_commit_and_path()
960 960 commit = self._get_commit_or_redirect(commit_id)
961 961
962 962 metadata = self._get_nodelist_at_commit(
963 963 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
964 964 return {'nodes': [x for x in metadata]}
965 965
966 966 def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type):
967 967 items = []
968 968 for name, commit_id in branches_or_tags.items():
969 969 sym_ref = symbolic_reference(commit_id, name, f_path, ref_type)
970 970 items.append((sym_ref, name, ref_type))
971 971 return items
972 972
973 973 def _symbolic_reference(self, commit_id, name, f_path, ref_type):
974 974 return commit_id
975 975
976 976 def _symbolic_reference_svn(self, commit_id, name, f_path, ref_type):
977 977 return commit_id
978 978
979 979 # NOTE(dan): old code we used in "diff" mode compare
980 980 new_f_path = vcspath.join(name, f_path)
981 981 return f'{new_f_path}@{commit_id}'
982 982
983 983 def _get_node_history(self, commit_obj, f_path, commits=None):
984 984 """
985 985 get commit history for given node
986 986
987 987 :param commit_obj: commit to calculate history
988 988 :param f_path: path for node to calculate history for
989 989 :param commits: if passed don't calculate history and take
990 990 commits defined in this list
991 991 """
992 992 _ = self.request.translate
993 993
994 994 # calculate history based on tip
995 995 tip = self.rhodecode_vcs_repo.get_commit()
996 996 if commits is None:
997 997 pre_load = ["author", "branch"]
998 998 try:
999 999 commits = tip.get_path_history(f_path, pre_load=pre_load)
1000 1000 except (NodeDoesNotExistError, CommitError):
1001 1001 # this node is not present at tip!
1002 1002 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
1003 1003
1004 1004 history = []
1005 1005 commits_group = ([], _("Changesets"))
1006 1006 for commit in commits:
1007 1007 branch = ' (%s)' % commit.branch if commit.branch else ''
1008 1008 n_desc = f'r{commit.idx}:{commit.short_id}{branch}'
1009 1009 commits_group[0].append((commit.raw_id, n_desc, 'sha'))
1010 1010 history.append(commits_group)
1011 1011
1012 1012 symbolic_reference = self._symbolic_reference
1013 1013
1014 1014 if self.rhodecode_vcs_repo.alias == 'svn':
1015 1015 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
1016 1016 f_path, self.rhodecode_vcs_repo)
1017 1017 if adjusted_f_path != f_path:
1018 1018 log.debug(
1019 1019 'Recognized svn tag or branch in file "%s", using svn '
1020 1020 'specific symbolic references', f_path)
1021 1021 f_path = adjusted_f_path
1022 1022 symbolic_reference = self._symbolic_reference_svn
1023 1023
1024 1024 branches = self._create_references(
1025 1025 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch')
1026 1026 branches_group = (branches, _("Branches"))
1027 1027
1028 1028 tags = self._create_references(
1029 1029 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag')
1030 1030 tags_group = (tags, _("Tags"))
1031 1031
1032 1032 history.append(branches_group)
1033 1033 history.append(tags_group)
1034 1034
1035 1035 return history, commits
1036 1036
1037 1037 @LoginRequired()
1038 1038 @HasRepoPermissionAnyDecorator(
1039 1039 'repository.read', 'repository.write', 'repository.admin')
1040 1040 def repo_file_history(self):
1041 1041 self.load_default_context()
1042 1042
1043 1043 commit_id, f_path = self._get_commit_and_path()
1044 1044 commit = self._get_commit_or_redirect(commit_id)
1045 1045 file_node = self._get_filenode_or_redirect(commit, f_path)
1046 1046
1047 1047 if file_node.is_file():
1048 1048 file_history, _hist = self._get_node_history(commit, f_path)
1049 1049
1050 1050 res = []
1051 1051 for section_items, section in file_history:
1052 1052 items = []
1053 1053 for obj_id, obj_text, obj_type in section_items:
1054 1054 at_rev = ''
1055 1055 if obj_type in ['branch', 'bookmark', 'tag']:
1056 1056 at_rev = obj_text
1057 1057 entry = {
1058 1058 'id': obj_id,
1059 1059 'text': obj_text,
1060 1060 'type': obj_type,
1061 1061 'at_rev': at_rev
1062 1062 }
1063 1063
1064 1064 items.append(entry)
1065 1065
1066 1066 res.append({
1067 1067 'text': section,
1068 1068 'children': items
1069 1069 })
1070 1070
1071 1071 data = {
1072 1072 'more': False,
1073 1073 'results': res
1074 1074 }
1075 1075 return data
1076 1076
1077 1077 log.warning('Cannot fetch history for directory')
1078 1078 raise HTTPBadRequest()
1079 1079
1080 1080 @LoginRequired()
1081 1081 @HasRepoPermissionAnyDecorator(
1082 1082 'repository.read', 'repository.write', 'repository.admin')
1083 1083 def repo_file_authors(self):
1084 1084 c = self.load_default_context()
1085 1085
1086 1086 commit_id, f_path = self._get_commit_and_path()
1087 1087 commit = self._get_commit_or_redirect(commit_id)
1088 1088 file_node = self._get_filenode_or_redirect(commit, f_path)
1089 1089
1090 1090 if not file_node.is_file():
1091 1091 raise HTTPBadRequest()
1092 1092
1093 1093 c.file_last_commit = file_node.last_commit
1094 1094 if self.request.GET.get('annotate') == '1':
1095 1095 # use _hist from annotation if annotation mode is on
1096 1096 commit_ids = {x[1] for x in file_node.annotate}
1097 1097 _hist = (
1098 1098 self.rhodecode_vcs_repo.get_commit(commit_id)
1099 1099 for commit_id in commit_ids)
1100 1100 else:
1101 1101 _f_history, _hist = self._get_node_history(commit, f_path)
1102 1102 c.file_author = False
1103 1103
1104 1104 unique = collections.OrderedDict()
1105 1105 for commit in _hist:
1106 1106 author = commit.author
1107 1107 if author not in unique:
1108 1108 unique[commit.author] = [
1109 1109 h.email(author),
1110 1110 h.person(author, 'username_or_name_or_email'),
1111 1111 1 # counter
1112 1112 ]
1113 1113
1114 1114 else:
1115 1115 # increase counter
1116 1116 unique[commit.author][2] += 1
1117 1117
1118 1118 c.authors = [val for val in unique.values()]
1119 1119
1120 1120 return self._get_template_context(c)
1121 1121
1122 1122 @LoginRequired()
1123 1123 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1124 1124 def repo_files_check_head(self):
1125 1125 self.load_default_context()
1126 1126
1127 1127 commit_id, f_path = self._get_commit_and_path()
1128 1128 _branch_name, _sha_commit_id, is_head = \
1129 1129 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1130 1130 landing_ref=self.db_repo.landing_ref_name)
1131 1131
1132 1132 new_path = self.request.POST.get('path')
1133 1133 operation = self.request.POST.get('operation')
1134 1134 path_exist = ''
1135 1135
1136 1136 if new_path and operation in ['create', 'upload']:
1137 1137 new_f_path = os.path.join(f_path.lstrip('/'), new_path)
1138 1138 try:
1139 1139 commit_obj = self.rhodecode_vcs_repo.get_commit(commit_id)
1140 1140 # NOTE(dan): construct whole path without leading /
1141 1141 file_node = commit_obj.get_node(new_f_path)
1142 1142 if file_node is not None:
1143 1143 path_exist = new_f_path
1144 1144 except EmptyRepositoryError:
1145 1145 pass
1146 1146 except Exception:
1147 1147 pass
1148 1148
1149 1149 return {
1150 1150 'branch': _branch_name,
1151 1151 'sha': _sha_commit_id,
1152 1152 'is_head': is_head,
1153 1153 'path_exists': path_exist
1154 1154 }
1155 1155
1156 1156 @LoginRequired()
1157 1157 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1158 1158 def repo_files_remove_file(self):
1159 1159 _ = self.request.translate
1160 1160 c = self.load_default_context()
1161 1161 commit_id, f_path = self._get_commit_and_path()
1162 1162
1163 1163 self._ensure_not_locked()
1164 1164 _branch_name, _sha_commit_id, is_head = \
1165 1165 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1166 1166 landing_ref=self.db_repo.landing_ref_name)
1167 1167
1168 1168 self.forbid_non_head(is_head, f_path)
1169 1169 self.check_branch_permission(_branch_name)
1170 1170
1171 1171 c.commit = self._get_commit_or_redirect(commit_id)
1172 1172 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1173 1173
1174 1174 c.default_message = _(
1175 1175 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1176 1176 c.f_path = f_path
1177 1177
1178 1178 return self._get_template_context(c)
1179 1179
1180 1180 @LoginRequired()
1181 1181 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1182 1182 @CSRFRequired()
1183 1183 def repo_files_delete_file(self):
1184 1184 _ = self.request.translate
1185 1185
1186 1186 c = self.load_default_context()
1187 1187 commit_id, f_path = self._get_commit_and_path()
1188 1188
1189 1189 self._ensure_not_locked()
1190 1190 _branch_name, _sha_commit_id, is_head = \
1191 1191 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1192 1192 landing_ref=self.db_repo.landing_ref_name)
1193 1193
1194 1194 self.forbid_non_head(is_head, f_path)
1195 1195 self.check_branch_permission(_branch_name)
1196 1196
1197 1197 c.commit = self._get_commit_or_redirect(commit_id)
1198 1198 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1199 1199
1200 1200 c.default_message = _(
1201 1201 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1202 1202 c.f_path = f_path
1203 1203 node_path = f_path
1204 1204 author = self._rhodecode_db_user.full_contact
1205 1205 message = self.request.POST.get('message') or c.default_message
1206 1206 try:
1207 1207 nodes = {
1208 1208 safe_bytes(node_path): {
1209 1209 'content': b''
1210 1210 }
1211 1211 }
1212 1212 ScmModel().delete_nodes(
1213 1213 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1214 1214 message=message,
1215 1215 nodes=nodes,
1216 1216 parent_commit=c.commit,
1217 1217 author=author,
1218 1218 )
1219 1219
1220 1220 h.flash(
1221 1221 _('Successfully deleted file `{}`').format(
1222 1222 h.escape(f_path)), category='success')
1223 1223 except Exception:
1224 1224 log.exception('Error during commit operation')
1225 1225 h.flash(_('Error occurred during commit'), category='error')
1226 1226 raise HTTPFound(
1227 1227 h.route_path('repo_commit', repo_name=self.db_repo_name,
1228 1228 commit_id='tip'))
1229 1229
1230 1230 @LoginRequired()
1231 1231 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1232 1232 def repo_files_edit_file(self):
1233 1233 _ = self.request.translate
1234 1234 c = self.load_default_context()
1235 1235 commit_id, f_path = self._get_commit_and_path()
1236 1236
1237 1237 self._ensure_not_locked()
1238 1238 _branch_name, _sha_commit_id, is_head = \
1239 1239 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1240 1240 landing_ref=self.db_repo.landing_ref_name)
1241 1241
1242 1242 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1243 1243 self.check_branch_permission(_branch_name, commit_id=commit_id)
1244 1244
1245 1245 c.commit = self._get_commit_or_redirect(commit_id)
1246 1246 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1247 1247
1248 1248 if c.file.is_binary:
1249 1249 files_url = h.route_path(
1250 1250 'repo_files',
1251 1251 repo_name=self.db_repo_name,
1252 1252 commit_id=c.commit.raw_id, f_path=f_path)
1253 1253 raise HTTPFound(files_url)
1254 1254
1255 1255 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1256 1256 c.f_path = f_path
1257 1257
1258 1258 return self._get_template_context(c)
1259 1259
1260 1260 @LoginRequired()
1261 1261 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1262 1262 @CSRFRequired()
1263 1263 def repo_files_update_file(self):
1264 1264 _ = self.request.translate
1265 1265 c = self.load_default_context()
1266 1266 commit_id, f_path = self._get_commit_and_path()
1267 1267
1268 1268 self._ensure_not_locked()
1269 1269
1270 1270 c.commit = self._get_commit_or_redirect(commit_id)
1271 1271 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1272 1272
1273 1273 if c.file.is_binary:
1274 1274 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name,
1275 1275 commit_id=c.commit.raw_id, f_path=f_path))
1276 1276
1277 1277 _branch_name, _sha_commit_id, is_head = \
1278 1278 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1279 1279 landing_ref=self.db_repo.landing_ref_name)
1280 1280
1281 1281 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1282 1282 self.check_branch_permission(_branch_name, commit_id=commit_id)
1283 1283
1284 1284 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1285 1285 c.f_path = f_path
1286 1286
1287 1287 old_content = c.file.str_content
1288 1288 sl = old_content.splitlines(1)
1289 1289 first_line = sl[0] if sl else ''
1290 1290
1291 1291 r_post = self.request.POST
1292 1292 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1293 1293 line_ending_mode = detect_mode(first_line, 0)
1294 1294 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1295 1295
1296 1296 message = r_post.get('message') or c.default_message
1297 1297
1298 1298 org_node_path = c.file.str_path
1299 1299 filename = r_post['filename']
1300 1300
1301 1301 root_path = c.file.dir_path
1302 1302 pure_path = self.create_pure_path(root_path, filename)
1303 1303 node_path = pure_path.as_posix()
1304 1304
1305 1305 default_redirect_url = h.route_path('repo_commit', repo_name=self.db_repo_name,
1306 1306 commit_id=commit_id)
1307 1307 if content == old_content and node_path == org_node_path:
1308 1308 h.flash(_('No changes detected on {}').format(h.escape(org_node_path)),
1309 1309 category='warning')
1310 1310 raise HTTPFound(default_redirect_url)
1311 1311
1312 1312 try:
1313 1313 mapping = {
1314 1314 c.file.bytes_path: {
1315 1315 'org_filename': org_node_path,
1316 1316 'filename': safe_bytes(node_path),
1317 1317 'content': safe_bytes(content),
1318 1318 'lexer': '',
1319 1319 'op': 'mod',
1320 1320 'mode': c.file.mode
1321 1321 }
1322 1322 }
1323 1323
1324 1324 commit = ScmModel().update_nodes(
1325 1325 user=self._rhodecode_db_user.user_id,
1326 1326 repo=self.db_repo,
1327 1327 message=message,
1328 1328 nodes=mapping,
1329 1329 parent_commit=c.commit,
1330 1330 )
1331 1331
1332 1332 h.flash(_('Successfully committed changes to file `{}`').format(
1333 1333 h.escape(f_path)), category='success')
1334 1334 default_redirect_url = h.route_path(
1335 1335 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1336 1336
1337 1337 except Exception:
1338 1338 log.exception('Error occurred during commit')
1339 1339 h.flash(_('Error occurred during commit'), category='error')
1340 1340
1341 1341 raise HTTPFound(default_redirect_url)
1342 1342
1343 1343 @LoginRequired()
1344 1344 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1345 1345 def repo_files_add_file(self):
1346 1346 _ = self.request.translate
1347 1347 c = self.load_default_context()
1348 1348 commit_id, f_path = self._get_commit_and_path()
1349 1349
1350 1350 self._ensure_not_locked()
1351 1351
1352 1352 # Check if we need to use this page to upload binary
1353 1353 upload_binary = str2bool(self.request.params.get('upload_binary', False))
1354 1354
1355 1355 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1356 1356 if c.commit is None:
1357 1357 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1358 1358
1359 1359 if self.rhodecode_vcs_repo.is_empty():
1360 1360 # for empty repository we cannot check for current branch, we rely on
1361 1361 # c.commit.branch instead
1362 1362 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1363 1363 else:
1364 1364 _branch_name, _sha_commit_id, is_head = \
1365 1365 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1366 1366 landing_ref=self.db_repo.landing_ref_name)
1367 1367
1368 1368 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1369 1369 self.check_branch_permission(_branch_name, commit_id=commit_id)
1370 1370
1371 1371 c.default_message = (_('Added file via RhodeCode Enterprise')) \
1372 1372 if not upload_binary else (_('Edited file {} via RhodeCode Enterprise').format(f_path))
1373 1373 c.f_path = f_path.lstrip('/') # ensure not relative path
1374 1374 c.replace_binary = upload_binary
1375 1375
1376 1376 return self._get_template_context(c)
1377 1377
1378 1378 @LoginRequired()
1379 1379 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1380 1380 @CSRFRequired()
1381 1381 def repo_files_create_file(self):
1382 1382 _ = self.request.translate
1383 1383 c = self.load_default_context()
1384 1384 commit_id, f_path = self._get_commit_and_path()
1385 1385
1386 1386 self._ensure_not_locked()
1387 1387
1388 1388 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1389 1389 if c.commit is None:
1390 1390 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1391 1391
1392 1392 # calculate redirect URL
1393 1393 if self.rhodecode_vcs_repo.is_empty():
1394 1394 default_redirect_url = h.route_path(
1395 1395 'repo_summary', repo_name=self.db_repo_name)
1396 1396 else:
1397 1397 default_redirect_url = h.route_path(
1398 1398 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1399 1399
1400 1400 if self.rhodecode_vcs_repo.is_empty():
1401 1401 # for empty repository we cannot check for current branch, we rely on
1402 1402 # c.commit.branch instead
1403 1403 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1404 1404 else:
1405 1405 _branch_name, _sha_commit_id, is_head = \
1406 1406 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1407 1407 landing_ref=self.db_repo.landing_ref_name)
1408 1408
1409 1409 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1410 1410 self.check_branch_permission(_branch_name, commit_id=commit_id)
1411 1411
1412 1412 c.default_message = (_('Added file via RhodeCode Enterprise'))
1413 1413 c.f_path = f_path
1414 1414
1415 1415 r_post = self.request.POST
1416 1416 message = r_post.get('message') or c.default_message
1417 1417 filename = r_post.get('filename')
1418 1418 unix_mode = 0
1419 1419
1420 1420 if not filename:
1421 1421 # If there's no commit, redirect to repo summary
1422 1422 if type(c.commit) is EmptyCommit:
1423 1423 redirect_url = h.route_path(
1424 1424 'repo_summary', repo_name=self.db_repo_name)
1425 1425 else:
1426 1426 redirect_url = default_redirect_url
1427 1427 h.flash(_('No filename specified'), category='warning')
1428 1428 raise HTTPFound(redirect_url)
1429 1429
1430 1430 root_path = f_path
1431 1431 pure_path = self.create_pure_path(root_path, filename)
1432 1432 node_path = pure_path.as_posix().lstrip('/')
1433 1433
1434 1434 author = self._rhodecode_db_user.full_contact
1435 1435 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1436 1436 nodes = {
1437 1437 safe_bytes(node_path): {
1438 1438 'content': safe_bytes(content)
1439 1439 }
1440 1440 }
1441 1441
1442 1442 try:
1443 1443
1444 1444 commit = ScmModel().create_nodes(
1445 1445 user=self._rhodecode_db_user.user_id,
1446 1446 repo=self.db_repo,
1447 1447 message=message,
1448 1448 nodes=nodes,
1449 1449 parent_commit=c.commit,
1450 1450 author=author,
1451 1451 )
1452 1452
1453 1453 h.flash(_('Successfully committed new file `{}`').format(
1454 1454 h.escape(node_path)), category='success')
1455 1455
1456 1456 default_redirect_url = h.route_path(
1457 1457 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1458 1458
1459 1459 except NonRelativePathError:
1460 1460 log.exception('Non Relative path found')
1461 1461 h.flash(_('The location specified must be a relative path and must not '
1462 1462 'contain .. in the path'), category='warning')
1463 1463 raise HTTPFound(default_redirect_url)
1464 1464 except (NodeError, NodeAlreadyExistsError) as e:
1465 1465 h.flash(h.escape(safe_str(e)), category='error')
1466 1466 except Exception:
1467 1467 log.exception('Error occurred during commit')
1468 1468 h.flash(_('Error occurred during commit'), category='error')
1469 1469
1470 1470 raise HTTPFound(default_redirect_url)
1471 1471
1472 1472 @LoginRequired()
1473 1473 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1474 1474 @CSRFRequired()
1475 1475 def repo_files_upload_file(self):
1476 1476 _ = self.request.translate
1477 1477 c = self.load_default_context()
1478 1478 commit_id, f_path = self._get_commit_and_path()
1479 1479
1480 1480 self._ensure_not_locked()
1481 1481
1482 1482 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1483 1483 if c.commit is None:
1484 1484 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1485 1485
1486 1486 # calculate redirect URL
1487 1487 if self.rhodecode_vcs_repo.is_empty():
1488 1488 default_redirect_url = h.route_path(
1489 1489 'repo_summary', repo_name=self.db_repo_name)
1490 1490 else:
1491 1491 default_redirect_url = h.route_path(
1492 1492 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1493 1493
1494 1494 if self.rhodecode_vcs_repo.is_empty():
1495 1495 # for empty repository we cannot check for current branch, we rely on
1496 1496 # c.commit.branch instead
1497 1497 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1498 1498 else:
1499 1499 _branch_name, _sha_commit_id, is_head = \
1500 1500 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1501 1501 landing_ref=self.db_repo.landing_ref_name)
1502 1502
1503 1503 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1504 1504 if error:
1505 1505 return {
1506 1506 'error': error,
1507 1507 'redirect_url': default_redirect_url
1508 1508 }
1509 1509 error = self.check_branch_permission(_branch_name, json_mode=True)
1510 1510 if error:
1511 1511 return {
1512 1512 'error': error,
1513 1513 'redirect_url': default_redirect_url
1514 1514 }
1515 1515
1516 1516 c.default_message = (_('Added file via RhodeCode Enterprise'))
1517 1517 c.f_path = f_path
1518 1518
1519 1519 r_post = self.request.POST
1520 1520
1521 1521 message = c.default_message
1522 1522 user_message = r_post.getall('message')
1523 1523 if isinstance(user_message, list) and user_message:
1524 1524 # we take the first from duplicated results if it's not empty
1525 1525 message = user_message[0] if user_message[0] else message
1526 1526
1527 1527 nodes = {}
1528 1528
1529 1529 for file_obj in r_post.getall('files_upload') or []:
1530 1530 content = file_obj.file
1531 1531 filename = file_obj.filename
1532 1532
1533 1533 root_path = f_path
1534 1534 pure_path = self.create_pure_path(root_path, filename)
1535 1535 node_path = pure_path.as_posix().lstrip('/')
1536 1536
1537 1537 nodes[safe_bytes(node_path)] = {
1538 1538 'content': content
1539 1539 }
1540 1540
1541 1541 if not nodes:
1542 1542 error = 'missing files'
1543 1543 return {
1544 1544 'error': error,
1545 1545 'redirect_url': default_redirect_url
1546 1546 }
1547 1547
1548 1548 author = self._rhodecode_db_user.full_contact
1549 1549
1550 1550 try:
1551 1551 commit = ScmModel().create_nodes(
1552 1552 user=self._rhodecode_db_user.user_id,
1553 1553 repo=self.db_repo,
1554 1554 message=message,
1555 1555 nodes=nodes,
1556 1556 parent_commit=c.commit,
1557 1557 author=author,
1558 1558 )
1559 1559 if len(nodes) == 1:
1560 1560 flash_message = _('Successfully committed {} new files').format(len(nodes))
1561 1561 else:
1562 1562 flash_message = _('Successfully committed 1 new file')
1563 1563
1564 1564 h.flash(flash_message, category='success')
1565 1565
1566 1566 default_redirect_url = h.route_path(
1567 1567 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1568 1568
1569 1569 except NonRelativePathError:
1570 1570 log.exception('Non Relative path found')
1571 1571 error = _('The location specified must be a relative path and must not '
1572 1572 'contain .. in the path')
1573 1573 h.flash(error, category='warning')
1574 1574
1575 1575 return {
1576 1576 'error': error,
1577 1577 'redirect_url': default_redirect_url
1578 1578 }
1579 1579 except (NodeError, NodeAlreadyExistsError) as e:
1580 1580 error = h.escape(e)
1581 1581 h.flash(error, category='error')
1582 1582
1583 1583 return {
1584 1584 'error': error,
1585 1585 'redirect_url': default_redirect_url
1586 1586 }
1587 1587 except Exception:
1588 1588 log.exception('Error occurred during commit')
1589 1589 error = _('Error occurred during commit')
1590 1590 h.flash(error, category='error')
1591 1591 return {
1592 1592 'error': error,
1593 1593 'redirect_url': default_redirect_url
1594 1594 }
1595 1595
1596 1596 return {
1597 1597 'error': None,
1598 1598 'redirect_url': default_redirect_url
1599 1599 }
1600 1600
1601 1601 @LoginRequired()
1602 1602 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1603 1603 @CSRFRequired()
1604 1604 def repo_files_replace_file(self):
1605 1605 _ = self.request.translate
1606 1606 c = self.load_default_context()
1607 1607 commit_id, f_path = self._get_commit_and_path()
1608 1608
1609 1609 self._ensure_not_locked()
1610 1610
1611 1611 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1612 1612 if c.commit is None:
1613 1613 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1614 1614
1615 1615 if self.rhodecode_vcs_repo.is_empty():
1616 1616 default_redirect_url = h.route_path(
1617 1617 'repo_summary', repo_name=self.db_repo_name)
1618 1618 else:
1619 1619 default_redirect_url = h.route_path(
1620 1620 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1621 1621
1622 1622 if self.rhodecode_vcs_repo.is_empty():
1623 1623 # for empty repository we cannot check for current branch, we rely on
1624 1624 # c.commit.branch instead
1625 1625 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1626 1626 else:
1627 1627 _branch_name, _sha_commit_id, is_head = \
1628 1628 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1629 1629 landing_ref=self.db_repo.landing_ref_name)
1630 1630
1631 1631 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1632 1632 if error:
1633 1633 return {
1634 1634 'error': error,
1635 1635 'redirect_url': default_redirect_url
1636 1636 }
1637 1637 error = self.check_branch_permission(_branch_name, json_mode=True)
1638 1638 if error:
1639 1639 return {
1640 1640 'error': error,
1641 1641 'redirect_url': default_redirect_url
1642 1642 }
1643 1643
1644 1644 c.default_message = (_('Edited file {} via RhodeCode Enterprise').format(f_path))
1645 1645 c.f_path = f_path
1646 1646
1647 1647 r_post = self.request.POST
1648 1648
1649 1649 message = c.default_message
1650 1650 user_message = r_post.getall('message')
1651 1651 if isinstance(user_message, list) and user_message:
1652 1652 # we take the first from duplicated results if it's not empty
1653 1653 message = user_message[0] if user_message[0] else message
1654 1654
1655 1655 data_for_replacement = r_post.getall('files_upload') or []
1656 1656 if (objects_count := len(data_for_replacement)) > 1:
1657 1657 return {
1658 1658 'error': 'too many files for replacement',
1659 1659 'redirect_url': default_redirect_url
1660 1660 }
1661 1661 elif not objects_count:
1662 1662 return {
1663 1663 'error': 'missing files',
1664 1664 'redirect_url': default_redirect_url
1665 1665 }
1666 1666
1667 1667 content = data_for_replacement[0].file
1668 1668 retrieved_filename = data_for_replacement[0].filename
1669 1669
1670 1670 if retrieved_filename.split('.')[-1] != f_path.split('.')[-1]:
1671 1671 return {
1672 1672 'error': 'file extension of uploaded file doesn\'t match an original file\'s extension',
1673 1673 'redirect_url': default_redirect_url
1674 1674 }
1675 1675
1676 1676 author = self._rhodecode_db_user.full_contact
1677 1677
1678 1678 try:
1679 1679 commit = ScmModel().update_binary_node(
1680 1680 user=self._rhodecode_db_user.user_id,
1681 1681 repo=self.db_repo,
1682 1682 message=message,
1683 1683 node={
1684 1684 'content': content,
1685 1685 'file_path': f_path.encode(),
1686 1686 },
1687 1687 parent_commit=c.commit,
1688 1688 author=author,
1689 1689 )
1690 1690
1691 1691 h.flash(_('Successfully committed 1 new file'), category='success')
1692 1692
1693 1693 default_redirect_url = h.route_path(
1694 1694 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1695 1695
1696 1696 except (NodeError, NodeAlreadyExistsError) as e:
1697 1697 error = h.escape(e)
1698 1698 h.flash(error, category='error')
1699 1699
1700 1700 return {
1701 1701 'error': error,
1702 1702 'redirect_url': default_redirect_url
1703 1703 }
1704 1704 except Exception:
1705 1705 log.exception('Error occurred during commit')
1706 1706 error = _('Error occurred during commit')
1707 1707 h.flash(error, category='error')
1708 1708 return {
1709 1709 'error': error,
1710 1710 'redirect_url': default_redirect_url
1711 1711 }
1712 1712
1713 1713 return {
1714 1714 'error': None,
1715 1715 'redirect_url': default_redirect_url
1716 1716 }
@@ -1,302 +1,309 b''
1 1 # Copyright (C) 2011-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import logging
20 20
21 21
22 22 from pyramid.httpexceptions import HTTPFound
23 23 from packaging.version import Version
24 24
25 25 from rhodecode import events
26 26 from rhodecode.apps._base import RepoAppView
27 27 from rhodecode.lib import helpers as h
28 28 from rhodecode.lib import audit_logger
29 29 from rhodecode.lib.auth import (
30 30 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired,
31 31 HasRepoPermissionAny)
32 from rhodecode.lib.exceptions import AttachedForksError, AttachedPullRequestsError
32 from rhodecode.lib.exceptions import AttachedForksError, AttachedPullRequestsError, AttachedArtifactsError
33 33 from rhodecode.lib.utils2 import safe_int
34 34 from rhodecode.lib.vcs import RepositoryError
35 35 from rhodecode.model.db import Session, UserFollowing, User, Repository
36 36 from rhodecode.model.permission import PermissionModel
37 37 from rhodecode.model.repo import RepoModel
38 38 from rhodecode.model.scm import ScmModel
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 class RepoSettingsAdvancedView(RepoAppView):
44 44
45 45 def load_default_context(self):
46 46 c = self._get_local_tmpl_context()
47 47 return c
48 48
49 49 def _get_users_with_permissions(self):
50 50 user_permissions = {}
51 51 for perm in self.db_repo.permissions():
52 52 user_permissions[perm.user_id] = perm
53 53
54 54 return user_permissions
55 55
56 56 @LoginRequired()
57 57 @HasRepoPermissionAnyDecorator('repository.admin')
58 58 def edit_advanced(self):
59 59 _ = self.request.translate
60 60 c = self.load_default_context()
61 61 c.active = 'advanced'
62 62
63 63 c.default_user_id = User.get_default_user_id()
64 64 c.in_public_journal = UserFollowing.query() \
65 65 .filter(UserFollowing.user_id == c.default_user_id) \
66 66 .filter(UserFollowing.follows_repository == self.db_repo).scalar()
67 67
68 68 c.ver_info_dict = self.rhodecode_vcs_repo.get_hooks_info()
69 69 c.hooks_outdated = False
70 70
71 71 try:
72 72 if Version(c.ver_info_dict['pre_version']) < Version(c.rhodecode_version):
73 73 c.hooks_outdated = True
74 74 except Exception:
75 75 pass
76 76
77 77 # update commit cache if GET flag is present
78 78 if self.request.GET.get('update_commit_cache'):
79 79 self.db_repo.update_commit_cache()
80 80 h.flash(_('updated commit cache'), category='success')
81 81
82 82 return self._get_template_context(c)
83 83
84 84 @LoginRequired()
85 85 @HasRepoPermissionAnyDecorator('repository.admin')
86 86 @CSRFRequired()
87 87 def edit_advanced_archive(self):
88 88 """
89 89 Archives the repository. It will become read-only, and not visible in search
90 90 or other queries. But still visible for super-admins.
91 91 """
92 92
93 93 _ = self.request.translate
94 94
95 95 try:
96 96 old_data = self.db_repo.get_api_data()
97 97 RepoModel().archive(self.db_repo)
98 98
99 99 repo = audit_logger.RepoWrap(repo_id=None, repo_name=self.db_repo.repo_name)
100 100 audit_logger.store_web(
101 101 'repo.archive', action_data={'old_data': old_data},
102 102 user=self._rhodecode_user, repo=repo)
103 103
104 104 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
105 105 h.flash(
106 106 _('Archived repository `%s`') % self.db_repo_name,
107 107 category='success')
108 108 Session().commit()
109 109 except Exception:
110 110 log.exception("Exception during archiving of repository")
111 111 h.flash(_('An error occurred during archiving of `%s`')
112 112 % self.db_repo_name, category='error')
113 113 # redirect to advanced for more deletion options
114 114 raise HTTPFound(
115 115 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name,
116 116 _anchor='advanced-archive'))
117 117
118 118 # flush permissions for all users defined in permissions
119 119 affected_user_ids = self._get_users_with_permissions().keys()
120 120 PermissionModel().trigger_permission_flush(affected_user_ids)
121 121
122 122 raise HTTPFound(h.route_path('home'))
123 123
124 124 @LoginRequired()
125 125 @HasRepoPermissionAnyDecorator('repository.admin')
126 126 @CSRFRequired()
127 127 def edit_advanced_delete(self):
128 128 """
129 129 Deletes the repository, or shows warnings if deletion is not possible
130 130 because of attached forks or other errors.
131 131 """
132 132 _ = self.request.translate
133 133 handle_forks = self.request.POST.get('forks', None)
134 134 if handle_forks == 'detach_forks':
135 135 handle_forks = 'detach'
136 136 elif handle_forks == 'delete_forks':
137 137 handle_forks = 'delete'
138 138
139 repo_advanced_url = h.route_path(
140 'edit_repo_advanced', repo_name=self.db_repo_name,
141 _anchor='advanced-delete')
139 142 try:
140 143 old_data = self.db_repo.get_api_data()
141 144 RepoModel().delete(self.db_repo, forks=handle_forks)
142 145
143 146 _forks = self.db_repo.forks.count()
144 147 if _forks and handle_forks:
145 148 if handle_forks == 'detach_forks':
146 149 h.flash(_('Detached %s forks') % _forks, category='success')
147 150 elif handle_forks == 'delete_forks':
148 151 h.flash(_('Deleted %s forks') % _forks, category='success')
149 152
150 153 repo = audit_logger.RepoWrap(repo_id=None, repo_name=self.db_repo.repo_name)
151 154 audit_logger.store_web(
152 155 'repo.delete', action_data={'old_data': old_data},
153 156 user=self._rhodecode_user, repo=repo)
154 157
155 158 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
156 159 h.flash(
157 160 _('Deleted repository `%s`') % self.db_repo_name,
158 161 category='success')
159 162 Session().commit()
160 163 except AttachedForksError:
161 repo_advanced_url = h.route_path(
162 'edit_repo_advanced', repo_name=self.db_repo_name,
163 _anchor='advanced-delete')
164 164 delete_anchor = h.link_to(_('detach or delete'), repo_advanced_url)
165 165 h.flash(_('Cannot delete `{repo}` it still contains attached forks. '
166 166 'Try using {delete_or_detach} option.')
167 167 .format(repo=self.db_repo_name, delete_or_detach=delete_anchor),
168 168 category='warning')
169 169
170 170 # redirect to advanced for forks handle action ?
171 171 raise HTTPFound(repo_advanced_url)
172 172
173 173 except AttachedPullRequestsError:
174 repo_advanced_url = h.route_path(
175 'edit_repo_advanced', repo_name=self.db_repo_name,
176 _anchor='advanced-delete')
177 174 attached_prs = len(self.db_repo.pull_requests_source +
178 175 self.db_repo.pull_requests_target)
179 176 h.flash(
180 177 _('Cannot delete `{repo}` it still contains {num} attached pull requests. '
181 178 'Consider archiving the repository instead.').format(
182 179 repo=self.db_repo_name, num=attached_prs), category='warning')
183 180
184 181 # redirect to advanced for forks handle action ?
185 182 raise HTTPFound(repo_advanced_url)
186 183
184 except AttachedArtifactsError:
185
186 attached_artifacts = len(self.db_repo.artifacts)
187 h.flash(
188 _('Cannot delete `{repo}` it still contains {num} attached artifacts. '
189 'Consider archiving the repository instead.').format(
190 repo=self.db_repo_name, num=attached_artifacts), category='warning')
191
192 # redirect to advanced for forks handle action ?
193 raise HTTPFound(repo_advanced_url)
187 194 except Exception:
188 195 log.exception("Exception during deletion of repository")
189 196 h.flash(_('An error occurred during deletion of `%s`')
190 197 % self.db_repo_name, category='error')
191 198 # redirect to advanced for more deletion options
192 199 raise HTTPFound(
193 200 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name,
194 201 _anchor='advanced-delete'))
195 202
196 203 raise HTTPFound(h.route_path('home'))
197 204
198 205 @LoginRequired()
199 206 @HasRepoPermissionAnyDecorator('repository.admin')
200 207 @CSRFRequired()
201 208 def edit_advanced_journal(self):
202 209 """
203 210 Set's this repository to be visible in public journal,
204 211 in other words making default user to follow this repo
205 212 """
206 213 _ = self.request.translate
207 214
208 215 try:
209 216 user_id = User.get_default_user_id()
210 217 ScmModel().toggle_following_repo(self.db_repo.repo_id, user_id)
211 218 h.flash(_('Updated repository visibility in public journal'),
212 219 category='success')
213 220 Session().commit()
214 221 except Exception:
215 222 h.flash(_('An error occurred during setting this '
216 223 'repository in public journal'),
217 224 category='error')
218 225
219 226 raise HTTPFound(
220 227 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
221 228
222 229 @LoginRequired()
223 230 @HasRepoPermissionAnyDecorator('repository.admin')
224 231 @CSRFRequired()
225 232 def edit_advanced_fork(self):
226 233 """
227 234 Mark given repository as a fork of another
228 235 """
229 236 _ = self.request.translate
230 237
231 238 new_fork_id = safe_int(self.request.POST.get('id_fork_of'))
232 239
233 240 # valid repo, re-check permissions
234 241 if new_fork_id:
235 242 repo = Repository.get(new_fork_id)
236 243 # ensure we have at least read access to the repo we mark
237 244 perm_check = HasRepoPermissionAny(
238 245 'repository.read', 'repository.write', 'repository.admin')
239 246
240 247 if repo and perm_check(repo_name=repo.repo_name):
241 248 new_fork_id = repo.repo_id
242 249 else:
243 250 new_fork_id = None
244 251
245 252 try:
246 253 repo = ScmModel().mark_as_fork(
247 254 self.db_repo_name, new_fork_id, self._rhodecode_user.user_id)
248 255 fork = repo.fork.repo_name if repo.fork else _('Nothing')
249 256 Session().commit()
250 257 h.flash(
251 258 _('Marked repo %s as fork of %s') % (self.db_repo_name, fork),
252 259 category='success')
253 260 except RepositoryError as e:
254 261 log.exception("Repository Error occurred")
255 262 h.flash(str(e), category='error')
256 263 except Exception:
257 264 log.exception("Exception while editing fork")
258 265 h.flash(_('An error occurred during this operation'),
259 266 category='error')
260 267
261 268 raise HTTPFound(
262 269 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
263 270
264 271 @LoginRequired()
265 272 @HasRepoPermissionAnyDecorator('repository.admin')
266 273 @CSRFRequired()
267 274 def edit_advanced_toggle_locking(self):
268 275 """
269 276 Toggle locking of repository
270 277 """
271 278 _ = self.request.translate
272 279 set_lock = self.request.POST.get('set_lock')
273 280 set_unlock = self.request.POST.get('set_unlock')
274 281
275 282 try:
276 283 if set_lock:
277 284 Repository.lock(self.db_repo, self._rhodecode_user.user_id,
278 285 lock_reason=Repository.LOCK_WEB)
279 286 h.flash(_('Locked repository'), category='success')
280 287 elif set_unlock:
281 288 Repository.unlock(self.db_repo)
282 289 h.flash(_('Unlocked repository'), category='success')
283 290 except Exception as e:
284 291 log.exception("Exception during unlocking")
285 292 h.flash(_('An error occurred during unlocking'), category='error')
286 293
287 294 raise HTTPFound(
288 295 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
289 296
290 297 @LoginRequired()
291 298 @HasRepoPermissionAnyDecorator('repository.admin')
292 299 def edit_advanced_install_hooks(self):
293 300 """
294 301 Install Hooks for repository
295 302 """
296 303 _ = self.request.translate
297 304 self.load_default_context()
298 305 self.rhodecode_vcs_repo.install_hooks(force=True)
299 306 h.flash(_('installed updated hooks into this repository'),
300 307 category='success')
301 308 raise HTTPFound(
302 309 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
@@ -1,60 +1,60 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import logging
20 20
21 21 from . import config_keys
22 22
23 23 from rhodecode.config.settings_maker import SettingsMaker
24 24
25 25 log = logging.getLogger(__name__)
26 26
27 27
28 28 def _sanitize_settings_and_apply_defaults(settings):
29 29 """
30 30 Set defaults, convert to python types and validate settings.
31 31 """
32 32 settings_maker = SettingsMaker(settings)
33 33
34 34 settings_maker.make_setting(config_keys.generate_authorized_keyfile, False, parser='bool')
35 35 settings_maker.make_setting(config_keys.wrapper_allow_shell, False, parser='bool')
36 36 settings_maker.make_setting(config_keys.enable_debug_logging, False, parser='bool')
37 37 settings_maker.make_setting(config_keys.ssh_key_generator_enabled, True, parser='bool')
38 38
39 39 settings_maker.make_setting(config_keys.authorized_keys_file_path, '~/.ssh/authorized_keys_rhodecode')
40 settings_maker.make_setting(config_keys.wrapper_cmd, '')
40 settings_maker.make_setting(config_keys.wrapper_cmd, '/usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper-v2')
41 41 settings_maker.make_setting(config_keys.authorized_keys_line_ssh_opts, '')
42 42
43 43 settings_maker.make_setting(config_keys.ssh_hg_bin, '/usr/local/bin/rhodecode_bin/vcs_bin/hg')
44 44 settings_maker.make_setting(config_keys.ssh_git_bin, '/usr/local/bin/rhodecode_bin/vcs_bin/git')
45 45 settings_maker.make_setting(config_keys.ssh_svn_bin, '/usr/local/bin/rhodecode_bin/vcs_bin/svnserve')
46 46
47 47 settings_maker.env_expand()
48 48
49 49
50 50 def includeme(config):
51 51 settings = config.registry.settings
52 52 _sanitize_settings_and_apply_defaults(settings)
53 53
54 54 # if we have enable generation of file, subscribe to event
55 55 if settings[config_keys.generate_authorized_keyfile]:
56 56 # lazy import here for faster code reading... via sshwrapper-v2 mode
57 57 from .subscribers import generate_ssh_authorized_keys_file_subscriber
58 58 from .events import SshKeyFileChangeEvent
59 59 config.add_subscriber(
60 60 generate_ssh_authorized_keys_file_subscriber, SshKeyFileChangeEvent)
@@ -1,32 +1,32 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 # Definition of setting keys used to configure this module. Defined here to
21 21 # avoid repetition of keys throughout the module.
22 22 generate_authorized_keyfile = 'ssh.generate_authorized_keyfile'
23 23 authorized_keys_file_path = 'ssh.authorized_keys_file_path'
24 24 authorized_keys_line_ssh_opts = 'ssh.authorized_keys_ssh_opts'
25 25 ssh_key_generator_enabled = 'ssh.enable_ui_key_generator'
26 wrapper_cmd = 'ssh.wrapper_cmd'
26 wrapper_cmd = 'ssh.wrapper_cmd.v2'
27 27 wrapper_allow_shell = 'ssh.wrapper_cmd_allow_shell'
28 28 enable_debug_logging = 'ssh.enable_debug_logging'
29 29
30 30 ssh_hg_bin = 'ssh.executable.hg'
31 31 ssh_git_bin = 'ssh.executable.git'
32 32 ssh_svn_bin = 'ssh.executable.svn'
@@ -1,175 +1,175 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import os
20 20 import sys
21 21 import logging
22 22
23 23 from rhodecode.lib.hook_daemon.base import prepare_callback_daemon
24 24 from rhodecode.lib.ext_json import sjson as json
25 25 from rhodecode.lib.vcs.conf import settings as vcs_settings
26 26 from rhodecode.lib.api_utils import call_service_api
27 27
28 28 log = logging.getLogger(__name__)
29 29
30 30
31 31 class SshVcsServer(object):
32 32 repo_user_agent = None # set in child classes
33 33 _path = None # set executable path for hg/git/svn binary
34 34 backend = None # set in child classes
35 35 tunnel = None # subprocess handling tunnel
36 36 settings = None # parsed settings module
37 37 write_perms = ['repository.admin', 'repository.write']
38 38 read_perms = ['repository.read', 'repository.admin', 'repository.write']
39 39
40 40 def __init__(self, user, user_permissions, settings, env):
41 41 self.user = user
42 42 self.user_permissions = user_permissions
43 43 self.settings = settings
44 44 self.env = env
45 45 self.stdin = sys.stdin
46 46
47 47 self.repo_name = None
48 48 self.repo_mode = None
49 49 self.store = ''
50 50 self.ini_path = ''
51 51 self.hooks_protocol = None
52 52
53 53 def _invalidate_cache(self, repo_name):
54 54 """
55 55 Set's cache for this repository for invalidation on next access
56 56
57 57 :param repo_name: full repo name, also a cache key
58 58 """
59 59 # Todo: Leave only "celery" case after transition.
60 60 match self.hooks_protocol:
61 61 case 'http':
62 62 from rhodecode.model.scm import ScmModel
63 63 ScmModel().mark_for_invalidation(repo_name)
64 64 case 'celery':
65 65 call_service_api(self.settings, {
66 66 "method": "service_mark_for_invalidation",
67 67 "args": {"repo_name": repo_name}
68 68 })
69 69
70 70 def has_write_perm(self):
71 71 permission = self.user_permissions.get(self.repo_name)
72 72 if permission in ['repository.write', 'repository.admin']:
73 73 return True
74 74
75 75 return False
76 76
77 77 def _check_permissions(self, action):
78 78 permission = self.user_permissions.get(self.repo_name)
79 79 user_info = f'{self.user["user_id"]}:{self.user["username"]}'
80 80 log.debug('permission for %s on %s are: %s',
81 81 user_info, self.repo_name, permission)
82 82
83 83 if not permission:
84 84 log.error('user `%s` permissions to repo:%s are empty. Forbidding access.',
85 85 user_info, self.repo_name)
86 86 return -2
87 87
88 88 if action == 'pull':
89 89 if permission in self.read_perms:
90 90 log.info(
91 91 'READ Permissions for User "%s" detected to repo "%s"!',
92 92 user_info, self.repo_name)
93 93 return 0
94 94 else:
95 95 if permission in self.write_perms:
96 96 log.info(
97 97 'WRITE, or Higher Permissions for User "%s" detected to repo "%s"!',
98 98 user_info, self.repo_name)
99 99 return 0
100 100
101 101 log.error('Cannot properly fetch or verify user `%s` permissions. '
102 102 'Permissions: %s, vcs action: %s',
103 103 user_info, permission, action)
104 104 return -2
105 105
106 106 def update_environment(self, action, extras=None):
107 107
108 108 scm_data = {
109 109 'ip': os.environ['SSH_CLIENT'].split()[0],
110 110 'username': self.user.username,
111 111 'user_id': self.user.user_id,
112 112 'action': action,
113 113 'repository': self.repo_name,
114 114 'scm': self.backend,
115 115 'config': self.ini_path,
116 116 'repo_store': self.store,
117 117 'make_lock': None,
118 118 'locked_by': [None, None],
119 119 'server_url': None,
120 120 'user_agent': f'{self.repo_user_agent}/ssh-user-agent',
121 121 'hooks': ['push', 'pull'],
122 122 'hooks_module': 'rhodecode.lib.hook_daemon.hook_module',
123 123 'is_shadow_repo': False,
124 124 'detect_force_push': False,
125 125 'check_branch_perms': False,
126 126
127 127 'SSH': True,
128 128 'SSH_PERMISSIONS': self.user_permissions.get(self.repo_name),
129 129 }
130 130 if extras:
131 131 scm_data.update(extras)
132 132 os.putenv("RC_SCM_DATA", json.dumps(scm_data))
133 133 return scm_data
134 134
135 135 def get_root_store(self):
136 136 root_store = self.store
137 137 if not root_store.endswith('/'):
138 138 # always append trailing slash
139 139 root_store = root_store + '/'
140 140 return root_store
141 141
142 142 def _handle_tunnel(self, extras):
143 143 # pre-auth
144 144 action = 'pull'
145 145 exit_code = self._check_permissions(action)
146 146 if exit_code:
147 147 return exit_code, False
148 148
149 149 req = self.env.get('request')
150 150 if req:
151 151 server_url = req.host_url + req.script_name
152 152 extras['server_url'] = server_url
153 153
154 154 log.debug('Using %s binaries from path %s', self.backend, self._path)
155 155 exit_code = self.tunnel.run(extras)
156 156
157 157 return exit_code, action == "push"
158 158
159 159 def run(self, tunnel_extras=None):
160 self.hooks_protocol = self.settings['vcs.hooks.protocol']
160 self.hooks_protocol = self.settings['vcs.hooks.protocol.v2']
161 161 tunnel_extras = tunnel_extras or {}
162 162 extras = {}
163 163 extras.update(tunnel_extras)
164 164
165 165 callback_daemon, extras = prepare_callback_daemon(
166 166 extras, protocol=self.hooks_protocol,
167 167 host=vcs_settings.HOOKS_HOST)
168 168
169 169 with callback_daemon:
170 170 try:
171 171 return self._handle_tunnel(extras)
172 172 finally:
173 173 log.debug('Running cleanup with cache invalidation')
174 174 if self.repo_name:
175 175 self._invalidate_cache(self.repo_name)
@@ -1,151 +1,151 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import os
20 20
21 21 import mock
22 22 import pytest
23 23
24 24 from rhodecode.apps.ssh_support.lib.backends.git import GitServer
25 25 from rhodecode.apps.ssh_support.tests.conftest import plain_dummy_env, plain_dummy_user
26 26 from rhodecode.lib.ext_json import json
27 27
28 28
29 29 class GitServerCreator(object):
30 30 root = '/tmp/repo/path/'
31 31 git_path = '/usr/local/bin/git'
32 32 config_data = {
33 33 'app:main': {
34 34 'ssh.executable.git': git_path,
35 'vcs.hooks.protocol': 'http',
35 'vcs.hooks.protocol.v2': 'celery',
36 36 }
37 37 }
38 38 repo_name = 'test_git'
39 39 repo_mode = 'receive-pack'
40 40 user = plain_dummy_user()
41 41
42 42 def __init__(self):
43 43 pass
44 44
45 45 def create(self, **kwargs):
46 46 parameters = {
47 47 'store': self.root,
48 48 'ini_path': '',
49 49 'user': self.user,
50 50 'repo_name': self.repo_name,
51 51 'repo_mode': self.repo_mode,
52 52 'user_permissions': {
53 53 self.repo_name: 'repository.admin'
54 54 },
55 55 'settings': self.config_data['app:main'],
56 56 'env': plain_dummy_env()
57 57 }
58 58 parameters.update(kwargs)
59 59 server = GitServer(**parameters)
60 60 return server
61 61
62 62
63 63 @pytest.fixture()
64 64 def git_server(app):
65 65 return GitServerCreator()
66 66
67 67
68 68 class TestGitServer(object):
69 69
70 70 def test_command(self, git_server):
71 71 server = git_server.create()
72 72 expected_command = (
73 73 'cd {root}; {git_path} {repo_mode} \'{root}{repo_name}\''.format(
74 74 root=git_server.root, git_path=git_server.git_path,
75 75 repo_mode=git_server.repo_mode, repo_name=git_server.repo_name)
76 76 )
77 77 assert expected_command == server.tunnel.command()
78 78
79 79 @pytest.mark.parametrize('permissions, action, code', [
80 80 ({}, 'pull', -2),
81 81 ({'test_git': 'repository.read'}, 'pull', 0),
82 82 ({'test_git': 'repository.read'}, 'push', -2),
83 83 ({'test_git': 'repository.write'}, 'push', 0),
84 84 ({'test_git': 'repository.admin'}, 'push', 0),
85 85
86 86 ])
87 87 def test_permission_checks(self, git_server, permissions, action, code):
88 88 server = git_server.create(user_permissions=permissions)
89 89 result = server._check_permissions(action)
90 90 assert result is code
91 91
92 92 @pytest.mark.parametrize('permissions, value', [
93 93 ({}, False),
94 94 ({'test_git': 'repository.read'}, False),
95 95 ({'test_git': 'repository.write'}, True),
96 96 ({'test_git': 'repository.admin'}, True),
97 97
98 98 ])
99 99 def test_has_write_permissions(self, git_server, permissions, value):
100 100 server = git_server.create(user_permissions=permissions)
101 101 result = server.has_write_perm()
102 102 assert result is value
103 103
104 104 def test_run_returns_executes_command(self, git_server):
105 105 server = git_server.create()
106 106 from rhodecode.apps.ssh_support.lib.backends.git import GitTunnelWrapper
107 107
108 108 os.environ['SSH_CLIENT'] = '127.0.0.1'
109 109 with mock.patch.object(GitTunnelWrapper, 'create_hooks_env') as _patch:
110 110 _patch.return_value = 0
111 111 with mock.patch.object(GitTunnelWrapper, 'command', return_value='date'):
112 112 exit_code = server.run()
113 113
114 114 assert exit_code == (0, False)
115 115
116 116 @pytest.mark.parametrize(
117 117 'repo_mode, action', [
118 118 ['receive-pack', 'push'],
119 119 ['upload-pack', 'pull']
120 120 ])
121 121 def test_update_environment(self, git_server, repo_mode, action):
122 122 server = git_server.create(repo_mode=repo_mode)
123 123 store = server.store
124 124
125 125 with mock.patch('os.environ', {'SSH_CLIENT': '10.10.10.10 b'}):
126 126 with mock.patch('os.putenv') as putenv_mock:
127 127 server.update_environment(action)
128 128
129 129 expected_data = {
130 130 'username': git_server.user.username,
131 131 'user_id': git_server.user.user_id,
132 132 'scm': 'git',
133 133 'repository': git_server.repo_name,
134 134 'make_lock': None,
135 135 'action': action,
136 136 'ip': '10.10.10.10',
137 137 'locked_by': [None, None],
138 138 'config': '',
139 139 'repo_store': store,
140 140 'server_url': None,
141 141 'hooks': ['push', 'pull'],
142 142 'is_shadow_repo': False,
143 143 'hooks_module': 'rhodecode.lib.hook_daemon.hook_module',
144 144 'check_branch_perms': False,
145 145 'detect_force_push': False,
146 146 'user_agent': u'git/ssh-user-agent',
147 147 'SSH': True,
148 148 'SSH_PERMISSIONS': 'repository.admin',
149 149 }
150 150 args, kwargs = putenv_mock.call_args
151 151 assert json.loads(args[1]) == expected_data
@@ -1,115 +1,115 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import os
20 20 import mock
21 21 import pytest
22 22
23 23 from rhodecode.apps.ssh_support.lib.backends.hg import MercurialServer
24 24 from rhodecode.apps.ssh_support.tests.conftest import plain_dummy_env, plain_dummy_user
25 25
26 26
27 27 class MercurialServerCreator(object):
28 28 root = '/tmp/repo/path/'
29 29 hg_path = '/usr/local/bin/hg'
30 30
31 31 config_data = {
32 32 'app:main': {
33 33 'ssh.executable.hg': hg_path,
34 'vcs.hooks.protocol': 'http',
34 'vcs.hooks.protocol.v2': 'celery',
35 35 }
36 36 }
37 37 repo_name = 'test_hg'
38 38 user = plain_dummy_user()
39 39
40 40 def __init__(self):
41 41 pass
42 42
43 43 def create(self, **kwargs):
44 44 parameters = {
45 45 'store': self.root,
46 46 'ini_path': '',
47 47 'user': self.user,
48 48 'repo_name': self.repo_name,
49 49 'user_permissions': {
50 50 'test_hg': 'repository.admin'
51 51 },
52 52 'settings': self.config_data['app:main'],
53 53 'env': plain_dummy_env()
54 54 }
55 55 parameters.update(kwargs)
56 56 server = MercurialServer(**parameters)
57 57 return server
58 58
59 59
60 60 @pytest.fixture()
61 61 def hg_server(app):
62 62 return MercurialServerCreator()
63 63
64 64
65 65 class TestMercurialServer(object):
66 66
67 67 def test_command(self, hg_server, tmpdir):
68 68 server = hg_server.create()
69 69 custom_hgrc = os.path.join(str(tmpdir), 'hgrc')
70 70 expected_command = (
71 71 'cd {root}; HGRCPATH={custom_hgrc} {hg_path} -R {root}{repo_name} serve --stdio'.format(
72 72 root=hg_server.root, custom_hgrc=custom_hgrc, hg_path=hg_server.hg_path,
73 73 repo_name=hg_server.repo_name)
74 74 )
75 75 server_command = server.tunnel.command(custom_hgrc)
76 76 assert expected_command == server_command
77 77
78 78 @pytest.mark.parametrize('permissions, action, code', [
79 79 ({}, 'pull', -2),
80 80 ({'test_hg': 'repository.read'}, 'pull', 0),
81 81 ({'test_hg': 'repository.read'}, 'push', -2),
82 82 ({'test_hg': 'repository.write'}, 'push', 0),
83 83 ({'test_hg': 'repository.admin'}, 'push', 0),
84 84
85 85 ])
86 86 def test_permission_checks(self, hg_server, permissions, action, code):
87 87 server = hg_server.create(user_permissions=permissions)
88 88 result = server._check_permissions(action)
89 89 assert result is code
90 90
91 91 @pytest.mark.parametrize('permissions, value', [
92 92 ({}, False),
93 93 ({'test_hg': 'repository.read'}, False),
94 94 ({'test_hg': 'repository.write'}, True),
95 95 ({'test_hg': 'repository.admin'}, True),
96 96
97 97 ])
98 98 def test_has_write_permissions(self, hg_server, permissions, value):
99 99 server = hg_server.create(user_permissions=permissions)
100 100 result = server.has_write_perm()
101 101 assert result is value
102 102
103 103 def test_run_returns_executes_command(self, hg_server):
104 104 server = hg_server.create()
105 105 from rhodecode.apps.ssh_support.lib.backends.hg import MercurialTunnelWrapper
106 106 os.environ['SSH_CLIENT'] = '127.0.0.1'
107 107 with mock.patch.object(MercurialTunnelWrapper, 'create_hooks_env') as _patch:
108 108 _patch.return_value = 0
109 109 with mock.patch.object(MercurialTunnelWrapper, 'command', return_value='date'):
110 110 exit_code = server.run()
111 111
112 112 assert exit_code == (0, False)
113 113
114 114
115 115
@@ -1,203 +1,203 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18 import os
19 19 import mock
20 20 import pytest
21 21
22 22 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionServer
23 23 from rhodecode.apps.ssh_support.tests.conftest import plain_dummy_env, plain_dummy_user
24 24
25 25
26 26 class SubversionServerCreator(object):
27 27 root = '/tmp/repo/path/'
28 28 svn_path = '/usr/local/bin/svnserve'
29 29 config_data = {
30 30 'app:main': {
31 31 'ssh.executable.svn': svn_path,
32 'vcs.hooks.protocol': 'http',
32 'vcs.hooks.protocol.v2': 'celery',
33 33 }
34 34 }
35 35 repo_name = 'test-svn'
36 36 user = plain_dummy_user()
37 37
38 38 def __init__(self):
39 39 pass
40 40
41 41 def create(self, **kwargs):
42 42 parameters = {
43 43 'store': self.root,
44 44 'repo_name': self.repo_name,
45 45 'ini_path': '',
46 46 'user': self.user,
47 47 'user_permissions': {
48 48 self.repo_name: 'repository.admin'
49 49 },
50 50 'settings': self.config_data['app:main'],
51 51 'env': plain_dummy_env()
52 52 }
53 53
54 54 parameters.update(kwargs)
55 55 server = SubversionServer(**parameters)
56 56 return server
57 57
58 58
59 59 @pytest.fixture()
60 60 def svn_server(app):
61 61 return SubversionServerCreator()
62 62
63 63
64 64 class TestSubversionServer(object):
65 65
66 66 def test_command(self, svn_server):
67 67 server = svn_server.create()
68 68 expected_command = [
69 69 svn_server.svn_path, '-t',
70 70 '--config-file', server.tunnel.svn_conf_path,
71 71 '--tunnel-user', svn_server.user.username,
72 72 '-r', svn_server.root
73 73 ]
74 74
75 75 assert expected_command == server.tunnel.command()
76 76
77 77 @pytest.mark.parametrize('permissions, action, code', [
78 78 ({}, 'pull', -2),
79 79 ({'test-svn': 'repository.read'}, 'pull', 0),
80 80 ({'test-svn': 'repository.read'}, 'push', -2),
81 81 ({'test-svn': 'repository.write'}, 'push', 0),
82 82 ({'test-svn': 'repository.admin'}, 'push', 0),
83 83
84 84 ])
85 85 def test_permission_checks(self, svn_server, permissions, action, code):
86 86 server = svn_server.create(user_permissions=permissions)
87 87 result = server._check_permissions(action)
88 88 assert result is code
89 89
90 90 @pytest.mark.parametrize('permissions, access_paths, expected_match', [
91 91 # not matched repository name
92 92 ({
93 93 'test-svn': ''
94 94 }, ['test-svn-1', 'test-svn-1/subpath'],
95 95 None),
96 96
97 97 # exact match
98 98 ({
99 99 'test-svn': ''
100 100 },
101 101 ['test-svn'],
102 102 'test-svn'),
103 103
104 104 # subdir commits
105 105 ({
106 106 'test-svn': ''
107 107 },
108 108 ['test-svn/foo',
109 109 'test-svn/foo/test-svn',
110 110 'test-svn/trunk/development.txt',
111 111 ],
112 112 'test-svn'),
113 113
114 114 # subgroups + similar patterns
115 115 ({
116 116 'test-svn': '',
117 117 'test-svn-1': '',
118 118 'test-svn-subgroup/test-svn': '',
119 119
120 120 },
121 121 ['test-svn-1',
122 122 'test-svn-1/foo/test-svn',
123 123 'test-svn-1/test-svn',
124 124 ],
125 125 'test-svn-1'),
126 126
127 127 # subgroups + similar patterns
128 128 ({
129 129 'test-svn-1': '',
130 130 'test-svn-10': '',
131 131 'test-svn-100': '',
132 132 },
133 133 ['test-svn-10',
134 134 'test-svn-10/foo/test-svn',
135 135 'test-svn-10/test-svn',
136 136 ],
137 137 'test-svn-10'),
138 138
139 139 # subgroups + similar patterns
140 140 ({
141 141 'name': '',
142 142 'nameContains': '',
143 143 'nameContainsThis': '',
144 144 },
145 145 ['nameContains',
146 146 'nameContains/This',
147 147 'nameContains/This/test-svn',
148 148 ],
149 149 'nameContains'),
150 150
151 151 # subgroups + similar patterns
152 152 ({
153 153 'test-svn': '',
154 154 'test-svn-1': '',
155 155 'test-svn-subgroup/test-svn': '',
156 156
157 157 },
158 158 ['test-svn-subgroup/test-svn',
159 159 'test-svn-subgroup/test-svn/foo/test-svn',
160 160 'test-svn-subgroup/test-svn/trunk/example.txt',
161 161 ],
162 162 'test-svn-subgroup/test-svn'),
163 163 ])
164 164 def test_repo_extraction_on_subdir(self, svn_server, permissions, access_paths, expected_match):
165 165 server = svn_server.create(user_permissions=permissions)
166 166 for path in access_paths:
167 167 repo_name = server.tunnel._match_repo_name(path)
168 168 assert repo_name == expected_match
169 169
170 170 def test_run_returns_executes_command(self, svn_server):
171 171 server = svn_server.create()
172 172 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper
173 173 os.environ['SSH_CLIENT'] = '127.0.0.1'
174 174 with mock.patch.object(
175 175 SubversionTunnelWrapper, 'get_first_client_response',
176 176 return_value={'url': 'http://server/test-svn'}):
177 177 with mock.patch.object(
178 178 SubversionTunnelWrapper, 'patch_first_client_response',
179 179 return_value=0):
180 180 with mock.patch.object(
181 181 SubversionTunnelWrapper, 'sync',
182 182 return_value=0):
183 183 with mock.patch.object(
184 184 SubversionTunnelWrapper, 'command',
185 185 return_value=['date']):
186 186
187 187 exit_code = server.run()
188 188 # SVN has this differently configured, and we get in our mock env
189 189 # None as return code
190 190 assert exit_code == (None, False)
191 191
192 192 def test_run_returns_executes_command_that_cannot_extract_repo_name(self, svn_server):
193 193 server = svn_server.create()
194 194 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper
195 195 with mock.patch.object(
196 196 SubversionTunnelWrapper, 'command',
197 197 return_value=['date']):
198 198 with mock.patch.object(
199 199 SubversionTunnelWrapper, 'get_first_client_response',
200 200 return_value=None):
201 201 exit_code = server.run()
202 202
203 203 assert exit_code == (1, False)
@@ -1,153 +1,154 b''
1 1 # Copyright (C) 2012-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import logging
20 20 import collections
21 21
22 22 from pyramid.exceptions import ConfigurationError
23 23
24 24 from rhodecode.lib.utils2 import safe_str
25 25 from rhodecode.model.settings import SettingsModel
26 26 from rhodecode.translation import _
27 27
28 28
29 29 log = logging.getLogger(__name__)
30 30
31 31
32 32 class AuthnResourceBase(object):
33 33 __name__ = None
34 34 __parent__ = None
35 35
36 36 def get_root(self):
37 37 current = self
38 38 while current.__parent__ is not None:
39 39 current = current.__parent__
40 40 return current
41 41
42 42
43 43 class AuthnPluginResourceBase(AuthnResourceBase):
44 44
45 45 def __init__(self, plugin):
46 46 self.plugin = plugin
47 47 self.__name__ = plugin.get_url_slug()
48 48 self.display_name = plugin.get_display_name()
49 49
50 50
51 51 class AuthnRootResource(AuthnResourceBase):
52 52 """
53 53 This is the root traversal resource object for the authentication settings.
54 54 """
55 is_root = True
55 56
56 57 def __init__(self):
57 58 self._store = collections.OrderedDict()
58 59 self._resource_name_map = {}
59 60 self.display_name = _('Authentication Plugins')
60 61
61 62 def __getitem__(self, key):
62 63 """
63 64 Customized get item function to return only items (plugins) that are
64 65 activated.
65 66 """
66 67 if self._is_item_active(key):
67 68 return self._store[key]
68 69 else:
69 70 raise KeyError('Authentication plugin "{}" is not active.'.format(
70 71 key))
71 72
72 73 def __iter__(self):
73 74 for key in self._store.keys():
74 75 if self._is_item_active(key):
75 76 yield self._store[key]
76 77
77 78 def _is_item_active(self, key):
78 79 activated_plugins = SettingsModel().get_auth_plugins()
79 80 plugin_id = self.get_plugin_id(key)
80 81 return plugin_id in activated_plugins
81 82
82 83 def get_plugin_id(self, resource_name):
83 84 """
84 85 Return the plugin id for the given traversal resource name.
85 86 """
86 87 # TODO: Store this info in the resource element.
87 88 return self._resource_name_map[resource_name]
88 89
89 90 def get_sorted_list(self, sort_key=None):
90 91 """
91 92 Returns a sorted list of sub resources for displaying purposes.
92 93 """
93 94 def default_sort_key(resource):
94 95 return str.lower(safe_str(resource.display_name))
95 96
96 97 active = [item for item in self]
97 98 return sorted(active, key=sort_key or default_sort_key)
98 99
99 100 def get_nav_list(self, sort=True):
100 101 """
101 102 Returns a sorted list of resources for displaying the navigation.
102 103 """
103 104 if sort:
104 105 nav_list = self.get_sorted_list()
105 106 else:
106 107 nav_list = [item for item in self]
107 108
108 109 nav_list.insert(0, self)
109 110 return nav_list
110 111
111 112 def add_authn_resource(self, config, plugin_id, resource):
112 113 """
113 114 Register a traversal resource as a sub element to the authentication
114 115 settings. This method is registered as a directive on the pyramid
115 116 configurator object and called by plugins.
116 117 """
117 118
118 119 def _ensure_unique_name(name, limit=100):
119 120 counter = 1
120 121 current = name
121 122 while current in self._store.keys():
122 123 current = f'{name}{counter}'
123 124 counter += 1
124 125 if counter > limit:
125 126 raise ConfigurationError(
126 127 'Cannot build unique name for traversal resource "%s" '
127 128 'registered by plugin "%s"', name, plugin_id)
128 129 return current
129 130
130 131 # Allow plugin resources with identical names by rename duplicates.
131 132 unique_name = _ensure_unique_name(resource.__name__)
132 133 if unique_name != resource.__name__:
133 134 log.warning('Name collision for traversal resource "%s" registered '
134 135 'by authentication plugin "%s"', resource.__name__,
135 136 plugin_id)
136 137 resource.__name__ = unique_name
137 138
138 139 log.debug('Register traversal resource "%s" for plugin "%s"',
139 140 unique_name, plugin_id)
140 141 self._resource_name_map[unique_name] = plugin_id
141 142 resource.__parent__ = self
142 143 self._store[unique_name] = resource
143 144
144 145
145 146 root = AuthnRootResource()
146 147
147 148
148 149 def root_factory(request=None):
149 150 """
150 151 Returns the root traversal resource instance used for the authentication
151 152 settings route.
152 153 """
153 154 return root
@@ -1,224 +1,237 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import os
20 20 import tempfile
21 21 import logging
22 22
23 23 from pyramid.settings import asbool
24 24
25 25 from rhodecode.config.settings_maker import SettingsMaker
26 26 from rhodecode.config import utils as config_utils
27 27
28 28 log = logging.getLogger(__name__)
29 29
30 30
31 31 def sanitize_settings_and_apply_defaults(global_config, settings):
32 32 """
33 33 Applies settings defaults and does all type conversion.
34 34
35 35 We would move all settings parsing and preparation into this place, so that
36 36 we have only one place left which deals with this part. The remaining parts
37 37 of the application would start to rely fully on well-prepared settings.
38 38
39 39 This piece would later be split up per topic to avoid a big fat monster
40 40 function.
41 41 """
42 42 jn = os.path.join
43 43
44 44 global_settings_maker = SettingsMaker(global_config)
45 45 global_settings_maker.make_setting('debug', default=False, parser='bool')
46 46 debug_enabled = asbool(global_config.get('debug'))
47 47
48 48 settings_maker = SettingsMaker(settings)
49 49
50 50 settings_maker.make_setting(
51 51 'logging.autoconfigure',
52 52 default=False,
53 53 parser='bool')
54 54
55 logging_conf = jn(os.path.dirname(global_config.get('__file__')), 'logging.ini')
55 ini_loc = os.path.dirname(global_config.get('__file__'))
56 logging_conf = jn(ini_loc, 'logging.ini')
56 57 settings_maker.enable_logging(logging_conf, level='INFO' if debug_enabled else 'DEBUG')
57 58
58 59 # Default includes, possible to change as a user
59 60 pyramid_includes = settings_maker.make_setting('pyramid.includes', [], parser='list:newline')
60 61 log.debug(
61 62 "Using the following pyramid.includes: %s",
62 63 pyramid_includes)
63 64
64 65 settings_maker.make_setting('rhodecode.edition', 'Community Edition')
65 66 settings_maker.make_setting('rhodecode.edition_id', 'CE')
66 67
67 68 if 'mako.default_filters' not in settings:
68 69 # set custom default filters if we don't have it defined
69 70 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
70 71 settings['mako.default_filters'] = 'h_filter'
71 72
72 73 if 'mako.directories' not in settings:
73 74 mako_directories = settings.setdefault('mako.directories', [
74 75 # Base templates of the original application
75 76 'rhodecode:templates',
76 77 ])
77 78 log.debug(
78 79 "Using the following Mako template directories: %s",
79 80 mako_directories)
80 81
81 82 # NOTE(marcink): fix redis requirement for schema of connection since 3.X
82 83 if 'beaker.session.type' in settings and settings['beaker.session.type'] == 'ext:redis':
83 84 raw_url = settings['beaker.session.url']
84 85 if not raw_url.startswith(('redis://', 'rediss://', 'unix://')):
85 86 settings['beaker.session.url'] = 'redis://' + raw_url
86 87
87 88 settings_maker.make_setting('__file__', global_config.get('__file__'))
88 89
89 90 # TODO: johbo: Re-think this, usually the call to config.include
90 91 # should allow to pass in a prefix.
91 92 settings_maker.make_setting('rhodecode.api.url', '/_admin/api')
92 93
93 94 # Sanitize generic settings.
94 95 settings_maker.make_setting('default_encoding', 'UTF-8', parser='list')
95 96 settings_maker.make_setting('gzip_responses', False, parser='bool')
96 97 settings_maker.make_setting('startup.import_repos', 'false', parser='bool')
97 98
99 # License settings.
100 settings_maker.make_setting('license.hide_license_info', False, parser='bool')
101 settings_maker.make_setting('license.import_path', '')
102 settings_maker.make_setting('license.import_path_mode', 'if-missing')
103
98 104 # statsd
99 105 settings_maker.make_setting('statsd.enabled', False, parser='bool')
100 106 settings_maker.make_setting('statsd.statsd_host', 'statsd-exporter', parser='string')
101 107 settings_maker.make_setting('statsd.statsd_port', 9125, parser='int')
102 108 settings_maker.make_setting('statsd.statsd_prefix', '')
103 109 settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool')
104 110
105 111 settings_maker.make_setting('vcs.svn.compatible_version', '')
106 112 settings_maker.make_setting('vcs.svn.redis_conn', 'redis://redis:6379/0')
107 113 settings_maker.make_setting('vcs.svn.proxy.enabled', True, parser='bool')
108 114 settings_maker.make_setting('vcs.svn.proxy.host', 'http://svn:8090', parser='string')
109 settings_maker.make_setting('vcs.hooks.protocol', 'http')
115 settings_maker.make_setting('vcs.hooks.protocol.v2', 'celery')
110 116 settings_maker.make_setting('vcs.hooks.host', '*')
111 117 settings_maker.make_setting('vcs.scm_app_implementation', 'http')
112 118 settings_maker.make_setting('vcs.server', '')
113 119 settings_maker.make_setting('vcs.server.protocol', 'http')
114 120 settings_maker.make_setting('vcs.server.enable', 'true', parser='bool')
115 121 settings_maker.make_setting('vcs.hooks.direct_calls', 'false', parser='bool')
116 122 settings_maker.make_setting('vcs.start_server', 'false', parser='bool')
117 123 settings_maker.make_setting('vcs.backends', 'hg, git, svn', parser='list')
118 124 settings_maker.make_setting('vcs.connection_timeout', 3600, parser='int')
125 settings_maker.make_setting('vcs.git.lfs.storage_location', '/var/opt/rhodecode_repo_store/.cache/git_lfs_store')
126 settings_maker.make_setting('vcs.hg.largefiles.storage_location',
127 '/var/opt/rhodecode_repo_store/.cache/hg_largefiles_store')
119 128
120 129 settings_maker.make_setting('vcs.methods.cache', True, parser='bool')
121 130
122 131 # repo_store path
123 132 settings_maker.make_setting('repo_store.path', '/var/opt/rhodecode_repo_store')
124 133 # Support legacy values of vcs.scm_app_implementation. Legacy
125 134 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http', or
126 135 # disabled since 4.13 'vcsserver.scm_app' which is now mapped to 'http'.
127 136 scm_app_impl = settings['vcs.scm_app_implementation']
128 137 if scm_app_impl in ['rhodecode.lib.middleware.utils.scm_app_http', 'vcsserver.scm_app']:
129 138 settings['vcs.scm_app_implementation'] = 'http'
130 139
131 140 settings_maker.make_setting('appenlight', False, parser='bool')
132 141
133 142 temp_store = tempfile.gettempdir()
134 143 tmp_cache_dir = jn(temp_store, 'rc_cache')
135 144
136 145 # save default, cache dir, and use it for all backends later.
137 146 default_cache_dir = settings_maker.make_setting(
138 147 'cache_dir',
139 148 default=tmp_cache_dir, default_when_empty=True,
140 149 parser='dir:ensured')
141 150
142 151 # exception store cache
143 152 settings_maker.make_setting(
144 153 'exception_tracker.store_path',
145 154 default=jn(default_cache_dir, 'exc_store'), default_when_empty=True,
146 155 parser='dir:ensured'
147 156 )
148 157
149 158 settings_maker.make_setting(
150 159 'celerybeat-schedule.path',
151 160 default=jn(default_cache_dir, 'celerybeat_schedule', 'celerybeat-schedule.db'), default_when_empty=True,
152 161 parser='file:ensured'
153 162 )
154 163
164 # celery
165 broker_url = settings_maker.make_setting('celery.broker_url', 'redis://redis:6379/8')
166 settings_maker.make_setting('celery.result_backend', broker_url)
167
155 168 settings_maker.make_setting('exception_tracker.send_email', False, parser='bool')
156 169 settings_maker.make_setting('exception_tracker.email_prefix', '[RHODECODE ERROR]', default_when_empty=True)
157 170
158 171 # sessions, ensure file since no-value is memory
159 172 settings_maker.make_setting('beaker.session.type', 'file')
160 173 settings_maker.make_setting('beaker.session.data_dir', jn(default_cache_dir, 'session_data'))
161 174
162 175 # cache_general
163 176 settings_maker.make_setting('rc_cache.cache_general.backend', 'dogpile.cache.rc.file_namespace')
164 177 settings_maker.make_setting('rc_cache.cache_general.expiration_time', 60 * 60 * 12, parser='int')
165 178 settings_maker.make_setting('rc_cache.cache_general.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_general.db'))
166 179
167 180 # cache_perms
168 181 settings_maker.make_setting('rc_cache.cache_perms.backend', 'dogpile.cache.rc.file_namespace')
169 182 settings_maker.make_setting('rc_cache.cache_perms.expiration_time', 60 * 60, parser='int')
170 183 settings_maker.make_setting('rc_cache.cache_perms.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_perms_db'))
171 184
172 185 # cache_repo
173 186 settings_maker.make_setting('rc_cache.cache_repo.backend', 'dogpile.cache.rc.file_namespace')
174 187 settings_maker.make_setting('rc_cache.cache_repo.expiration_time', 60 * 60 * 24 * 30, parser='int')
175 188 settings_maker.make_setting('rc_cache.cache_repo.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_repo_db'))
176 189
177 190 # cache_license
178 191 settings_maker.make_setting('rc_cache.cache_license.backend', 'dogpile.cache.rc.file_namespace')
179 192 settings_maker.make_setting('rc_cache.cache_license.expiration_time', 60 * 5, parser='int')
180 193 settings_maker.make_setting('rc_cache.cache_license.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_license_db'))
181 194
182 195 # cache_repo_longterm memory, 96H
183 196 settings_maker.make_setting('rc_cache.cache_repo_longterm.backend', 'dogpile.cache.rc.memory_lru')
184 197 settings_maker.make_setting('rc_cache.cache_repo_longterm.expiration_time', 345600, parser='int')
185 198 settings_maker.make_setting('rc_cache.cache_repo_longterm.max_size', 10000, parser='int')
186 199
187 200 # sql_cache_short
188 201 settings_maker.make_setting('rc_cache.sql_cache_short.backend', 'dogpile.cache.rc.memory_lru')
189 202 settings_maker.make_setting('rc_cache.sql_cache_short.expiration_time', 30, parser='int')
190 203 settings_maker.make_setting('rc_cache.sql_cache_short.max_size', 10000, parser='int')
191 204
192 205 # archive_cache
193 206 settings_maker.make_setting('archive_cache.locking.url', 'redis://redis:6379/1')
194 207 settings_maker.make_setting('archive_cache.backend.type', 'filesystem')
195 208
196 209 settings_maker.make_setting('archive_cache.filesystem.store_dir', jn(default_cache_dir, 'archive_cache'), default_when_empty=True,)
197 210 settings_maker.make_setting('archive_cache.filesystem.cache_shards', 8, parser='int')
198 211 settings_maker.make_setting('archive_cache.filesystem.cache_size_gb', 10, parser='float')
199 212 settings_maker.make_setting('archive_cache.filesystem.eviction_policy', 'least-recently-stored')
200 213
201 214 settings_maker.make_setting('archive_cache.filesystem.retry', False, parser='bool')
202 215 settings_maker.make_setting('archive_cache.filesystem.retry_backoff', 1, parser='int')
203 216 settings_maker.make_setting('archive_cache.filesystem.retry_attempts', 10, parser='int')
204 217
205 settings_maker.make_setting('archive_cache.objectstore.url', jn(default_cache_dir, 'archive_cache'), default_when_empty=True,)
218 settings_maker.make_setting('archive_cache.objectstore.url', 'http://s3-minio:9000', default_when_empty=True,)
206 219 settings_maker.make_setting('archive_cache.objectstore.key', '')
207 220 settings_maker.make_setting('archive_cache.objectstore.secret', '')
208 221 settings_maker.make_setting('archive_cache.objectstore.region', 'eu-central-1')
209 222 settings_maker.make_setting('archive_cache.objectstore.bucket', 'rhodecode-archive-cache', default_when_empty=True,)
210 223 settings_maker.make_setting('archive_cache.objectstore.bucket_shards', 8, parser='int')
211 224
212 225 settings_maker.make_setting('archive_cache.objectstore.cache_size_gb', 10, parser='float')
213 226 settings_maker.make_setting('archive_cache.objectstore.eviction_policy', 'least-recently-stored')
214 227
215 228 settings_maker.make_setting('archive_cache.objectstore.retry', False, parser='bool')
216 229 settings_maker.make_setting('archive_cache.objectstore.retry_backoff', 1, parser='int')
217 230 settings_maker.make_setting('archive_cache.objectstore.retry_attempts', 10, parser='int')
218 231
219 232 settings_maker.env_expand()
220 233
221 234 # configure instance id
222 235 config_utils.set_instance_id(settings)
223 236
224 237 return settings
@@ -1,87 +1,99 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 import os
20 19 import logging
20
21 21 import rhodecode
22 22 import collections
23 23
24 24 from rhodecode.config import utils
25 25
26 26 from rhodecode.lib.utils import load_rcextensions
27 27 from rhodecode.lib.utils2 import str2bool
28 28 from rhodecode.lib.vcs import connect_vcs
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32
33 def propagate_rhodecode_config(global_config, settings, config):
34 # Store the settings to make them available to other modules.
35 settings_merged = global_config.copy()
36 settings_merged.update(settings)
37 if config:
38 settings_merged.update(config)
39
40 rhodecode.PYRAMID_SETTINGS = settings_merged
41 rhodecode.CONFIG = settings_merged
42
43 if 'default_user_id' not in rhodecode.CONFIG:
44 rhodecode.CONFIG['default_user_id'] = utils.get_default_user_id()
45 log.debug('set rhodecode.CONFIG data')
46
47
33 48 def load_pyramid_environment(global_config, settings):
34 49 # Some parts of the code expect a merge of global and app settings.
35 50 settings_merged = global_config.copy()
36 51 settings_merged.update(settings)
37 52
38 53 # TODO(marcink): probably not required anymore
39 54 # configure channelstream,
40 55 settings_merged['channelstream_config'] = {
41 56 'enabled': str2bool(settings_merged.get('channelstream.enabled', False)),
42 57 'server': settings_merged.get('channelstream.server'),
43 58 'secret': settings_merged.get('channelstream.secret')
44 59 }
45 60
46 61 # If this is a test run we prepare the test environment like
47 62 # creating a test database, test search index and test repositories.
48 63 # This has to be done before the database connection is initialized.
49 64 if rhodecode.is_test:
50 65 rhodecode.disable_error_handler = True
51 66 from rhodecode import authentication
52 67 authentication.plugin_default_auth_ttl = 0
53 68
54 69 utils.initialize_test_environment(settings_merged)
55 70
56 71 # Initialize the database connection.
57 72 utils.initialize_database(settings_merged)
58 73
59 74 load_rcextensions(root_path=settings_merged['here'])
60 75
61 76 # Limit backends to `vcs.backends` from configuration, and preserve the order
62 77 for alias in list(rhodecode.BACKENDS.keys()):
63 78 if alias not in settings['vcs.backends']:
64 79 del rhodecode.BACKENDS[alias]
65 80
66 81 _sorted_backend = sorted(rhodecode.BACKENDS.items(),
67 82 key=lambda item: settings['vcs.backends'].index(item[0]))
68 83 rhodecode.BACKENDS = collections.OrderedDict(_sorted_backend)
69 84
70 85 log.info('Enabled VCS backends: %s', list(rhodecode.BACKENDS.keys()))
71 86
72 87 # initialize vcs client and optionally run the server if enabled
73 88 vcs_server_uri = settings['vcs.server']
74 89 vcs_server_enabled = settings['vcs.server.enable']
75 90
76 91 utils.configure_vcs(settings)
77 92
78 # Store the settings to make them available to other modules.
79
80 rhodecode.PYRAMID_SETTINGS = settings_merged
81 rhodecode.CONFIG = settings_merged
82 rhodecode.CONFIG['default_user_id'] = utils.get_default_user_id()
93 # first run, to store data...
94 propagate_rhodecode_config(global_config, settings, {})
83 95
84 96 if vcs_server_enabled:
85 97 connect_vcs(vcs_server_uri, utils.get_vcs_server_protocol(settings))
86 98 else:
87 99 log.warning('vcs-server not enabled, vcs connection unavailable')
@@ -1,466 +1,471 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import os
20 20 import sys
21 21 import collections
22 22
23 23 import time
24 24 import logging.config
25 25
26 26 from paste.gzipper import make_gzip_middleware
27 27 import pyramid.events
28 28 from pyramid.wsgi import wsgiapp
29 29 from pyramid.config import Configurator
30 30 from pyramid.settings import asbool, aslist
31 31 from pyramid.httpexceptions import (
32 32 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
33 33 from pyramid.renderers import render_to_response
34 34
35 35 from rhodecode.model import meta
36 36 from rhodecode.config import patches
37 37
38 from rhodecode.config.environment import load_pyramid_environment
38 from rhodecode.config.environment import load_pyramid_environment, propagate_rhodecode_config
39 39
40 40 import rhodecode.events
41 41 from rhodecode.config.config_maker import sanitize_settings_and_apply_defaults
42 42 from rhodecode.lib.middleware.vcs import VCSMiddleware
43 43 from rhodecode.lib.request import Request
44 44 from rhodecode.lib.vcs import VCSCommunicationError
45 45 from rhodecode.lib.exceptions import VCSServerUnavailable
46 46 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
47 47 from rhodecode.lib.middleware.https_fixup import HttpsFixup
48 48 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
49 49 from rhodecode.lib.utils2 import AttributeDict
50 50 from rhodecode.lib.exc_tracking import store_exception, format_exc
51 51 from rhodecode.subscribers import (
52 52 scan_repositories_if_enabled, write_js_routes_if_enabled,
53 write_metadata_if_needed, write_usage_data)
53 write_metadata_if_needed, write_usage_data, import_license_if_present)
54 54 from rhodecode.lib.statsd_client import StatsdClient
55 55
56 56 log = logging.getLogger(__name__)
57 57
58 58
59 59 def is_http_error(response):
60 60 # error which should have traceback
61 61 return response.status_code > 499
62 62
63 63
64 64 def should_load_all():
65 65 """
66 66 Returns if all application components should be loaded. In some cases it's
67 67 desired to skip apps loading for faster shell script execution
68 68 """
69 69 ssh_cmd = os.environ.get('RC_CMD_SSH_WRAPPER')
70 70 if ssh_cmd:
71 71 return False
72 72
73 73 return True
74 74
75 75
76 76 def make_pyramid_app(global_config, **settings):
77 77 """
78 78 Constructs the WSGI application based on Pyramid.
79 79
80 80 Specials:
81 81
82 82 * The application can also be integrated like a plugin via the call to
83 83 `includeme`. This is accompanied with the other utility functions which
84 84 are called. Changing this should be done with great care to not break
85 85 cases when these fragments are assembled from another place.
86 86
87 87 """
88 88 start_time = time.time()
89 89 log.info('Pyramid app config starting')
90 90
91 91 sanitize_settings_and_apply_defaults(global_config, settings)
92 92
93 93 # init and bootstrap StatsdClient
94 94 StatsdClient.setup(settings)
95 95
96 96 config = Configurator(settings=settings)
97 97 # Init our statsd at very start
98 98 config.registry.statsd = StatsdClient.statsd
99 99
100 100 # Apply compatibility patches
101 101 patches.inspect_getargspec()
102 patches.repoze_sendmail_lf_fix()
102 103
103 104 load_pyramid_environment(global_config, settings)
104 105
105 106 # Static file view comes first
106 107 includeme_first(config)
107 108
108 109 includeme(config)
109 110
110 111 pyramid_app = config.make_wsgi_app()
111 112 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
112 113 pyramid_app.config = config
113 114
114 115 celery_settings = get_celery_config(settings)
115 116 config.configure_celery(celery_settings)
116 117
118 # final config set...
119 propagate_rhodecode_config(global_config, settings, config.registry.settings)
120
117 121 # creating the app uses a connection - return it after we are done
118 122 meta.Session.remove()
119 123
120 124 total_time = time.time() - start_time
121 125 log.info('Pyramid app created and configured in %.2fs', total_time)
122 126 return pyramid_app
123 127
124 128
125 129 def get_celery_config(settings):
126 130 """
127 131 Converts basic ini configuration into celery 4.X options
128 132 """
129 133
130 134 def key_converter(key_name):
131 135 pref = 'celery.'
132 136 if key_name.startswith(pref):
133 137 return key_name[len(pref):].replace('.', '_').lower()
134 138
135 139 def type_converter(parsed_key, value):
136 140 # cast to int
137 141 if value.isdigit():
138 142 return int(value)
139 143
140 144 # cast to bool
141 145 if value.lower() in ['true', 'false', 'True', 'False']:
142 146 return value.lower() == 'true'
143 147 return value
144 148
145 149 celery_config = {}
146 150 for k, v in settings.items():
147 151 pref = 'celery.'
148 152 if k.startswith(pref):
149 153 celery_config[key_converter(k)] = type_converter(key_converter(k), v)
150 154
151 155 # TODO:rethink if we want to support celerybeat based file config, probably NOT
152 156 # beat_config = {}
153 157 # for section in parser.sections():
154 158 # if section.startswith('celerybeat:'):
155 159 # name = section.split(':', 1)[1]
156 160 # beat_config[name] = get_beat_config(parser, section)
157 161
158 162 # final compose of settings
159 163 celery_settings = {}
160 164
161 165 if celery_config:
162 166 celery_settings.update(celery_config)
163 167 # if beat_config:
164 168 # celery_settings.update({'beat_schedule': beat_config})
165 169
166 170 return celery_settings
167 171
168 172
169 173 def not_found_view(request):
170 174 """
171 175 This creates the view which should be registered as not-found-view to
172 176 pyramid.
173 177 """
174 178
175 179 if not getattr(request, 'vcs_call', None):
176 180 # handle like regular case with our error_handler
177 181 return error_handler(HTTPNotFound(), request)
178 182
179 183 # handle not found view as a vcs call
180 184 settings = request.registry.settings
181 185 ae_client = getattr(request, 'ae_client', None)
182 186 vcs_app = VCSMiddleware(
183 187 HTTPNotFound(), request.registry, settings,
184 188 appenlight_client=ae_client)
185 189
186 190 return wsgiapp(vcs_app)(None, request)
187 191
188 192
189 193 def error_handler(exception, request):
190 194 import rhodecode
191 195 from rhodecode.lib import helpers
192 196
193 197 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
194 198
195 199 base_response = HTTPInternalServerError()
196 200 # prefer original exception for the response since it may have headers set
197 201 if isinstance(exception, HTTPException):
198 202 base_response = exception
199 203 elif isinstance(exception, VCSCommunicationError):
200 204 base_response = VCSServerUnavailable()
201 205
202 206 if is_http_error(base_response):
203 207 traceback_info = format_exc(request.exc_info)
204 208 log.error(
205 209 'error occurred handling this request for path: %s, \n%s',
206 210 request.path, traceback_info)
207 211
208 212 error_explanation = base_response.explanation or str(base_response)
209 213 if base_response.status_code == 404:
210 214 error_explanation += " Optionally you don't have permission to access this page."
211 215 c = AttributeDict()
212 216 c.error_message = base_response.status
213 217 c.error_explanation = error_explanation
214 218 c.visual = AttributeDict()
215 219
216 220 c.visual.rhodecode_support_url = (
217 221 request.registry.settings.get('rhodecode_support_url') or
218 222 request.route_url('rhodecode_support')
219 223 )
220 224 c.redirect_time = 0
221 225 c.rhodecode_name = rhodecode_title
222 226 if not c.rhodecode_name:
223 227 c.rhodecode_name = 'Rhodecode'
224 228
225 229 c.causes = []
226 230 if is_http_error(base_response):
227 231 c.causes.append('Server is overloaded.')
228 232 c.causes.append('Server database connection is lost.')
229 233 c.causes.append('Server expected unhandled error.')
230 234
231 235 if hasattr(base_response, 'causes'):
232 236 c.causes = base_response.causes
233 237
234 238 c.messages = helpers.flash.pop_messages(request=request)
235 239 exc_info = sys.exc_info()
236 240 c.exception_id = id(exc_info)
237 241 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
238 242 or base_response.status_code > 499
239 243 c.exception_id_url = request.route_url(
240 244 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
241 245
242 246 debug_mode = rhodecode.ConfigGet().get_bool('debug')
243 247 if c.show_exception_id:
244 248 store_exception(c.exception_id, exc_info)
245 249 c.exception_debug = debug_mode
246 250 c.exception_config_ini = rhodecode.CONFIG.get('__file__')
247 251
248 252 if debug_mode:
249 253 try:
250 254 from rich.traceback import install
251 255 install(show_locals=True)
252 256 log.debug('Installing rich tracebacks...')
253 257 except ImportError:
254 258 pass
255 259
256 260 response = render_to_response(
257 261 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
258 262 response=base_response)
259 263
260 264 response.headers["X-RC-Exception-Id"] = str(c.exception_id)
261 265
262 266 statsd = request.registry.statsd
263 267 if statsd and base_response.status_code > 499:
264 268 exc_type = f"{exception.__class__.__module__}.{exception.__class__.__name__}"
265 269 statsd.incr('rhodecode_exception_total',
266 270 tags=["exc_source:web",
267 271 f"http_code:{base_response.status_code}",
268 272 f"type:{exc_type}"])
269 273
270 274 return response
271 275
272 276
273 277 def includeme_first(config):
274 278 # redirect automatic browser favicon.ico requests to correct place
275 279 def favicon_redirect(context, request):
276 280 return HTTPFound(
277 281 request.static_path('rhodecode:public/images/favicon.ico'))
278 282
279 283 config.add_view(favicon_redirect, route_name='favicon')
280 284 config.add_route('favicon', '/favicon.ico')
281 285
282 286 def robots_redirect(context, request):
283 287 return HTTPFound(
284 288 request.static_path('rhodecode:public/robots.txt'))
285 289
286 290 config.add_view(robots_redirect, route_name='robots')
287 291 config.add_route('robots', '/robots.txt')
288 292
289 293 config.add_static_view(
290 294 '_static/deform', 'deform:static')
291 295 config.add_static_view(
292 296 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
293 297
294 298
295 299 ce_auth_resources = [
296 300 'rhodecode.authentication.plugins.auth_crowd',
297 301 'rhodecode.authentication.plugins.auth_headers',
298 302 'rhodecode.authentication.plugins.auth_jasig_cas',
299 303 'rhodecode.authentication.plugins.auth_ldap',
300 304 'rhodecode.authentication.plugins.auth_pam',
301 305 'rhodecode.authentication.plugins.auth_rhodecode',
302 306 'rhodecode.authentication.plugins.auth_token',
303 307 ]
304 308
305 309
306 310 def includeme(config, auth_resources=None):
307 311 from rhodecode.lib.celerylib.loader import configure_celery
308 312 log.debug('Initializing main includeme from %s', os.path.basename(__file__))
309 313 settings = config.registry.settings
310 314 config.set_request_factory(Request)
311 315
312 316 # plugin information
313 317 config.registry.rhodecode_plugins = collections.OrderedDict()
314 318
315 319 config.add_directive(
316 320 'register_rhodecode_plugin', register_rhodecode_plugin)
317 321
318 322 config.add_directive('configure_celery', configure_celery)
319 323
320 324 if settings.get('appenlight', False):
321 325 config.include('appenlight_client.ext.pyramid_tween')
322 326
323 327 load_all = should_load_all()
324 328
325 329 # Includes which are required. The application would fail without them.
326 330 config.include('pyramid_mako')
327 331 config.include('rhodecode.lib.rc_beaker')
328 332 config.include('rhodecode.lib.rc_cache')
329 333 config.include('rhodecode.lib.archive_cache')
330 334
331 335 config.include('rhodecode.apps._base.navigation')
332 336 config.include('rhodecode.apps._base.subscribers')
333 337 config.include('rhodecode.tweens')
334 338 config.include('rhodecode.authentication')
335 339
336 340 if load_all:
337 341
338 342 # load CE authentication plugins
339 343
340 344 if auth_resources:
341 345 ce_auth_resources.extend(auth_resources)
342 346
343 347 for resource in ce_auth_resources:
344 348 config.include(resource)
345 349
346 350 # Auto discover authentication plugins and include their configuration.
347 351 if asbool(settings.get('auth_plugin.import_legacy_plugins', 'true')):
348 352 from rhodecode.authentication import discover_legacy_plugins
349 353 discover_legacy_plugins(config)
350 354
351 355 # apps
352 356 if load_all:
353 357 log.debug('Starting config.include() calls')
354 358 config.include('rhodecode.api.includeme')
355 359 config.include('rhodecode.apps._base.includeme')
356 360 config.include('rhodecode.apps._base.navigation.includeme')
357 361 config.include('rhodecode.apps._base.subscribers.includeme')
358 362 config.include('rhodecode.apps.hovercards.includeme')
359 363 config.include('rhodecode.apps.ops.includeme')
360 364 config.include('rhodecode.apps.channelstream.includeme')
361 365 config.include('rhodecode.apps.file_store.includeme')
362 366 config.include('rhodecode.apps.admin.includeme')
363 367 config.include('rhodecode.apps.login.includeme')
364 368 config.include('rhodecode.apps.home.includeme')
365 369 config.include('rhodecode.apps.journal.includeme')
366 370
367 371 config.include('rhodecode.apps.repository.includeme')
368 372 config.include('rhodecode.apps.repo_group.includeme')
369 373 config.include('rhodecode.apps.user_group.includeme')
370 374 config.include('rhodecode.apps.search.includeme')
371 375 config.include('rhodecode.apps.user_profile.includeme')
372 376 config.include('rhodecode.apps.user_group_profile.includeme')
373 377 config.include('rhodecode.apps.my_account.includeme')
374 378 config.include('rhodecode.apps.gist.includeme')
375 379
376 380 config.include('rhodecode.apps.svn_support.includeme')
377 381 config.include('rhodecode.apps.ssh_support.includeme')
378 382 config.include('rhodecode.apps.debug_style')
379 383
380 384 if load_all:
381 385 config.include('rhodecode.integrations.includeme')
382 386 config.include('rhodecode.integrations.routes.includeme')
383 387
384 388 config.add_route('rhodecode_support', 'https://rhodecode.com/help/', static=True)
385 389 settings['default_locale_name'] = settings.get('lang', 'en')
386 390 config.add_translation_dirs('rhodecode:i18n/')
387 391
388 392 # Add subscribers.
389 393 if load_all:
390 394 log.debug('Adding subscribers...')
391 395 config.add_subscriber(scan_repositories_if_enabled,
392 396 pyramid.events.ApplicationCreated)
393 397 config.add_subscriber(write_metadata_if_needed,
394 398 pyramid.events.ApplicationCreated)
395 399 config.add_subscriber(write_usage_data,
396 400 pyramid.events.ApplicationCreated)
397 401 config.add_subscriber(write_js_routes_if_enabled,
398 402 pyramid.events.ApplicationCreated)
399
403 config.add_subscriber(import_license_if_present,
404 pyramid.events.ApplicationCreated)
400 405
401 406 # Set the default renderer for HTML templates to mako.
402 407 config.add_mako_renderer('.html')
403 408
404 409 config.add_renderer(
405 410 name='json_ext',
406 411 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
407 412
408 413 config.add_renderer(
409 414 name='string_html',
410 415 factory='rhodecode.lib.string_renderer.html')
411 416
412 417 # include RhodeCode plugins
413 418 includes = aslist(settings.get('rhodecode.includes', []))
414 419 log.debug('processing rhodecode.includes data...')
415 420 for inc in includes:
416 421 config.include(inc)
417 422
418 423 # custom not found view, if our pyramid app doesn't know how to handle
419 424 # the request pass it to potential VCS handling ap
420 425 config.add_notfound_view(not_found_view)
421 426 if not settings.get('debugtoolbar.enabled', False):
422 427 # disabled debugtoolbar handle all exceptions via the error_handlers
423 428 config.add_view(error_handler, context=Exception)
424 429
425 430 # all errors including 403/404/50X
426 431 config.add_view(error_handler, context=HTTPError)
427 432
428 433
429 434 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
430 435 """
431 436 Apply outer WSGI middlewares around the application.
432 437 """
433 438 registry = config.registry
434 439 settings = registry.settings
435 440
436 441 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
437 442 pyramid_app = HttpsFixup(pyramid_app, settings)
438 443
439 444 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
440 445 pyramid_app, settings)
441 446 registry.ae_client = _ae_client
442 447
443 448 if settings['gzip_responses']:
444 449 pyramid_app = make_gzip_middleware(
445 450 pyramid_app, settings, compress_level=1)
446 451
447 452 # this should be the outer most middleware in the wsgi stack since
448 453 # middleware like Routes make database calls
449 454 def pyramid_app_with_cleanup(environ, start_response):
450 455 start = time.time()
451 456 try:
452 457 return pyramid_app(environ, start_response)
453 458 finally:
454 459 # Dispose current database session and rollback uncommitted
455 460 # transactions.
456 461 meta.Session.remove()
457 462
458 463 # In a single threaded mode server, on non sqlite db we should have
459 464 # '0 Current Checked out connections' at the end of a request,
460 465 # if not, then something, somewhere is leaving a connection open
461 466 pool = meta.get_engine().pool
462 467 log.debug('sa pool status: %s', pool.status())
463 468 total = time.time() - start
464 469 log.debug('Request processing finalized: %.4fs', total)
465 470
466 471 return pyramid_app_with_cleanup
@@ -1,160 +1,167 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 Compatibility patches.
21 21
22 22 Please keep the following principles in mind:
23 23
24 24 * Keep imports local, so that importing this module does not cause too many
25 25 side effects by itself.
26 26
27 27 * Try to make patches idempotent, calling them multiple times should not do
28 28 harm. If that is not possible, ensure that the second call explodes.
29 29
30 30 """
31 31
32 32
33 33 def inspect_formatargspec():
34 34
35 35 import inspect
36 36 from inspect import formatannotation
37 37
38 38 def backport_inspect_formatargspec(
39 39 args, varargs=None, varkw=None, defaults=None,
40 40 kwonlyargs=(), kwonlydefaults={}, annotations={},
41 41 formatarg=str,
42 42 formatvarargs=lambda name: '*' + name,
43 43 formatvarkw=lambda name: '**' + name,
44 44 formatvalue=lambda value: '=' + repr(value),
45 45 formatreturns=lambda text: ' -> ' + text,
46 46 formatannotation=formatannotation):
47 47 """Copy formatargspec from python 3.7 standard library.
48 48 Python 3 has deprecated formatargspec and requested that Signature
49 49 be used instead, however this requires a full reimplementation
50 50 of formatargspec() in terms of creating Parameter objects and such.
51 51 Instead of introducing all the object-creation overhead and having
52 52 to reinvent from scratch, just copy their compatibility routine.
53 53 Utimately we would need to rewrite our "decorator" routine completely
54 54 which is not really worth it right now, until all Python 2.x support
55 55 is dropped.
56 56 """
57 57
58 58 def formatargandannotation(arg):
59 59 result = formatarg(arg)
60 60 if arg in annotations:
61 61 result += ': ' + formatannotation(annotations[arg])
62 62 return result
63 63
64 64 specs = []
65 65 if defaults:
66 66 firstdefault = len(args) - len(defaults)
67 67 for i, arg in enumerate(args):
68 68 spec = formatargandannotation(arg)
69 69 if defaults and i >= firstdefault:
70 70 spec = spec + formatvalue(defaults[i - firstdefault])
71 71 specs.append(spec)
72 72 if varargs is not None:
73 73 specs.append(formatvarargs(formatargandannotation(varargs)))
74 74 else:
75 75 if kwonlyargs:
76 76 specs.append('*')
77 77 if kwonlyargs:
78 78 for kwonlyarg in kwonlyargs:
79 79 spec = formatargandannotation(kwonlyarg)
80 80 if kwonlydefaults and kwonlyarg in kwonlydefaults:
81 81 spec += formatvalue(kwonlydefaults[kwonlyarg])
82 82 specs.append(spec)
83 83 if varkw is not None:
84 84 specs.append(formatvarkw(formatargandannotation(varkw)))
85 85 result = '(' + ', '.join(specs) + ')'
86 86 if 'return' in annotations:
87 87 result += formatreturns(formatannotation(annotations['return']))
88 88 return result
89 89
90 90 # NOTE: inject for python3.11
91 91 inspect.formatargspec = backport_inspect_formatargspec
92 92 return inspect
93 93
94 94
95 95 def inspect_getargspec():
96 96 """
97 97 Pyramid rely on inspect.getargspec to lookup the signature of
98 98 view functions. This is not compatible with cython, therefore we replace
99 99 getargspec with a custom version.
100 100 Code is inspired by the inspect module from Python-3.4
101 101 """
102 102 import inspect
103 103
104 104 def _isCython(func):
105 105 """
106 106 Private helper that checks if a function is a cython function.
107 107 """
108 108 return func.__class__.__name__ == 'cython_function_or_method'
109 109
110 110 def unwrap(func):
111 111 """
112 112 Get the object wrapped by *func*.
113 113
114 114 Follows the chain of :attr:`__wrapped__` attributes returning the last
115 115 object in the chain.
116 116
117 117 *stop* is an optional callback accepting an object in the wrapper chain
118 118 as its sole argument that allows the unwrapping to be terminated early
119 119 if the callback returns a true value. If the callback never returns a
120 120 true value, the last object in the chain is returned as usual. For
121 121 example, :func:`signature` uses this to stop unwrapping if any object
122 122 in the chain has a ``__signature__`` attribute defined.
123 123
124 124 :exc:`ValueError` is raised if a cycle is encountered.
125 125 """
126 126 f = func # remember the original func for error reporting
127 127 memo = {id(f)} # Memoise by id to tolerate non-hashable objects
128 128 while hasattr(func, '__wrapped__'):
129 129 func = func.__wrapped__
130 130 id_func = id(func)
131 131 if id_func in memo:
132 132 raise ValueError(f'wrapper loop when unwrapping {f!r}')
133 133 memo.add(id_func)
134 134 return func
135 135
136 136 def custom_getargspec(func):
137 137 """
138 138 Get the names and default values of a function's arguments.
139 139
140 140 A tuple of four things is returned: (args, varargs, varkw, defaults).
141 141 'args' is a list of the argument names (it may contain nested lists).
142 142 'varargs' and 'varkw' are the names of the * and ** arguments or None.
143 143 'defaults' is an n-tuple of the default values of the last n arguments.
144 144 """
145 145
146 146 func = unwrap(func)
147 147
148 148 if inspect.ismethod(func):
149 149 func = func.im_func
150 150 if not inspect.isfunction(func):
151 151 if not _isCython(func):
152 152 raise TypeError('{!r} is not a Python or Cython function'
153 153 .format(func))
154 154 args, varargs, varkw = inspect.getargs(func.func_code)
155 155 return inspect.ArgSpec(args, varargs, varkw, func.func_defaults)
156 156
157 157 # NOTE: inject for python3.11
158 158 inspect.getargspec = inspect.getfullargspec
159 159
160 160 return inspect
161
162
163 def repoze_sendmail_lf_fix():
164 from repoze.sendmail import encoding
165 from email.policy import SMTP
166
167 encoding.encode_message = lambda message, *args, **kwargs: message.as_bytes(policy=SMTP)
@@ -1,110 +1,110 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import os
20 20 import platform
21 21
22 22 DEFAULT_USER = 'default'
23 23
24 24
25 25 def configure_vcs(config):
26 26 """
27 27 Patch VCS config with some RhodeCode specific stuff
28 28 """
29 29 from rhodecode.lib.vcs import conf
30 30 import rhodecode.lib.vcs.conf.settings
31 31
32 32 conf.settings.BACKENDS = {
33 33 'hg': 'rhodecode.lib.vcs.backends.hg.MercurialRepository',
34 34 'git': 'rhodecode.lib.vcs.backends.git.GitRepository',
35 35 'svn': 'rhodecode.lib.vcs.backends.svn.SubversionRepository',
36 36 }
37 37
38 conf.settings.HOOKS_PROTOCOL = config['vcs.hooks.protocol']
38 conf.settings.HOOKS_PROTOCOL = config['vcs.hooks.protocol.v2']
39 39 conf.settings.HOOKS_HOST = config['vcs.hooks.host']
40 40 conf.settings.DEFAULT_ENCODINGS = config['default_encoding']
41 41 conf.settings.ALIASES[:] = config['vcs.backends']
42 42 conf.settings.SVN_COMPATIBLE_VERSION = config['vcs.svn.compatible_version']
43 43
44 44
45 45 def initialize_database(config):
46 46 from rhodecode.lib.utils2 import engine_from_config, get_encryption_key
47 47 from rhodecode.model import init_model
48 48 engine = engine_from_config(config, 'sqlalchemy.db1.')
49 49 init_model(engine, encryption_key=get_encryption_key(config))
50 50
51 51
52 52 def initialize_test_environment(settings, test_env=None):
53 53 if test_env is None:
54 54 test_env = not int(os.environ.get('RC_NO_TMP_PATH', 0))
55 55
56 56 from rhodecode.lib.utils import (
57 57 create_test_directory, create_test_database, create_test_repositories,
58 58 create_test_index)
59 59 from rhodecode.tests import TESTS_TMP_PATH
60 60 from rhodecode.lib.vcs.backends.hg import largefiles_store
61 61 from rhodecode.lib.vcs.backends.git import lfs_store
62 62
63 63 # test repos
64 64 if test_env:
65 65 create_test_directory(TESTS_TMP_PATH)
66 66 # large object stores
67 67 create_test_directory(largefiles_store(TESTS_TMP_PATH))
68 68 create_test_directory(lfs_store(TESTS_TMP_PATH))
69 69
70 70 create_test_database(TESTS_TMP_PATH, settings)
71 71 create_test_repositories(TESTS_TMP_PATH, settings)
72 72 create_test_index(TESTS_TMP_PATH, settings)
73 73
74 74
75 75 def get_vcs_server_protocol(config):
76 76 return config['vcs.server.protocol']
77 77
78 78
79 79 def set_instance_id(config):
80 80 """
81 81 Sets a dynamic generated config['instance_id'] if missing or '*'
82 82 E.g instance_id = *cluster-1 or instance_id = *
83 83 """
84 84
85 85 config['instance_id'] = config.get('instance_id') or ''
86 86 instance_id = config['instance_id']
87 87 if instance_id.startswith('*') or not instance_id:
88 88 prefix = instance_id.lstrip('*')
89 89 _platform_id = platform.uname()[1] or 'instance'
90 90 config['instance_id'] = '{prefix}uname:{platform}-pid:{pid}'.format(
91 91 prefix=prefix,
92 92 platform=_platform_id,
93 93 pid=os.getpid())
94 94
95 95
96 96 def get_default_user_id():
97 97 from sqlalchemy import text
98 98 from rhodecode.model import meta
99 99
100 100 engine = meta.get_engine()
101 101 with meta.SA_Session(engine) as session:
102 102 result = session.execute(text(
103 103 "SELECT user_id from users where username = :uname"
104 104 ), {'uname': DEFAULT_USER})
105 105 user = result.first()
106 106 if not user:
107 107 raise ValueError('Unable to retrieve default user data from DB')
108 108 user_id = user[0]
109 109
110 110 return user_id
@@ -1,78 +1,80 b''
1 1 # Copyright (C) 2015-2024 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import logging
20 20
21 21 from .backends.fanout_cache import FileSystemFanoutCache
22 22 from .backends.objectstore_cache import ObjectStoreCache
23 23
24 24 from .utils import archive_iterator # noqa
25 25 from .lock import ArchiveCacheGenerationLock # noqa
26 26
27 27 log = logging.getLogger(__name__)
28 28
29 29
30 30 cache_meta = None
31 31
32 32
33 33 def includeme(config):
34 return # don't init cache currently for faster startup time
35
34 36 # init our cache at start
35 settings = config.get_settings()
36 get_archival_cache_store(settings)
37 # settings = config.get_settings()
38 # get_archival_cache_store(settings)
37 39
38 40
39 41 def get_archival_config(config):
40 42
41 43 final_config = {
42 44
43 45 }
44 46
45 47 for k, v in config.items():
46 48 if k.startswith('archive_cache'):
47 49 final_config[k] = v
48 50
49 51 return final_config
50 52
51 53
52 54 def get_archival_cache_store(config, always_init=False):
53 55
54 56 global cache_meta
55 57 if cache_meta is not None and not always_init:
56 58 return cache_meta
57 59
58 60 config = get_archival_config(config)
59 61 backend = config['archive_cache.backend.type']
60 62
61 63 archive_cache_locking_url = config['archive_cache.locking.url']
62 64
63 65 match backend:
64 66 case 'filesystem':
65 67 d_cache = FileSystemFanoutCache(
66 68 locking_url=archive_cache_locking_url,
67 69 **config
68 70 )
69 71 case 'objectstore':
70 72 d_cache = ObjectStoreCache(
71 73 locking_url=archive_cache_locking_url,
72 74 **config
73 75 )
74 76 case _:
75 77 raise ValueError(f'archive_cache.backend.type only supports "filesystem" or "objectstore" got {backend} ')
76 78
77 79 cache_meta = d_cache
78 80 return cache_meta
@@ -1,173 +1,173 b''
1 1 # Copyright (C) 2015-2024 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import codecs
20 20 import hashlib
21 21 import logging
22 22 import os
23 23 import typing
24 24
25 25 import fsspec
26 26
27 27 from .base import BaseCache, BaseShard
28 28 from ..utils import ShardFileReader, NOT_GIVEN
29 29 from ...type_utils import str2bool
30 30
31 31 log = logging.getLogger(__name__)
32 32
33 33
34 34 class S3Shard(BaseShard):
35 35
36 36 def __init__(self, index, bucket, bucket_folder, fs, **settings):
37 37 self._index: int = index
38 38 self._bucket_folder: str = bucket_folder
39 39 self.storage_type: str = 'bucket'
40 40 self._bucket_main: str = bucket
41 41
42 42 self.fs = fs
43 43
44 44 @property
45 45 def bucket(self) -> str:
46 46 """Cache bucket final path."""
47 47 return os.path.join(self._bucket_main, self._bucket_folder)
48 48
49 49 def _get_keyfile(self, archive_key) -> tuple[str, str]:
50 50 key_file: str = f'{archive_key}-{self.key_suffix}'
51 51 return key_file, os.path.join(self.bucket, key_file)
52 52
53 53 def _get_writer(self, path, mode):
54 54 return self.fs.open(path, 'wb')
55 55
56 56 def _write_file(self, full_path, iterator, mode):
57 57
58 58 # ensure folder in bucket exists
59 59 destination = self.bucket
60 60 if not self.fs.exists(destination):
61 self.fs.mkdir(destination, s3_additional_kwargs={})
61 self.fs.mkdir(destination)
62 62
63 63 writer = self._get_writer(full_path, mode)
64 64
65 65 digest = hashlib.sha256()
66 66 with writer:
67 67 size = 0
68 68 for chunk in iterator:
69 69 size += len(chunk)
70 70 digest.update(chunk)
71 71 writer.write(chunk)
72 72
73 73 sha256 = digest.hexdigest()
74 74 log.debug('written new archive cache under %s, sha256: %s', full_path, sha256)
75 75 return size, sha256
76 76
77 77 def store(self, key, value_reader, metadata: dict | None = None):
78 78 return self._store(key, value_reader, metadata, mode='wb')
79 79
80 80 def fetch(self, key, retry=NOT_GIVEN,
81 81 retry_attempts=NOT_GIVEN, retry_backoff=1,
82 82 presigned_url_expires: int = 0) -> tuple[ShardFileReader, dict]:
83 83 return self._fetch(key, retry, retry_attempts, retry_backoff, presigned_url_expires=presigned_url_expires)
84 84
85 85 def remove(self, key):
86 86 return self._remove(key)
87 87
88 88 def random_filename(self):
89 89 """Return filename and full-path tuple for file storage.
90 90
91 91 Filename will be a randomly generated 28 character hexadecimal string
92 92 with ".archive_cache" suffixed. Two levels of sub-directories will be used to
93 93 reduce the size of directories. On older filesystems, lookups in
94 94 directories with many files may be slow.
95 95 """
96 96
97 97 hex_name = codecs.encode(os.urandom(16), 'hex').decode('utf-8')
98 98
99 99 archive_name = hex_name[4:] + '.archive_cache'
100 100 filename = f"{hex_name[:2]}-{hex_name[2:4]}-{archive_name}"
101 101
102 102 full_path = os.path.join(self.bucket, filename)
103 103 return archive_name, full_path
104 104
105 105 def __repr__(self):
106 106 return f'{self.__class__.__name__}(index={self._index}, bucket={self.bucket})'
107 107
108 108
109 109 class ObjectStoreCache(BaseCache):
110 110 shard_name: str = 'shard-{:03d}'
111 111 shard_cls = S3Shard
112 112
113 113 def __init__(self, locking_url, **settings):
114 114 """
115 115 Initialize objectstore cache instance.
116 116
117 117 :param str locking_url: redis url for a lock
118 118 :param settings: settings dict
119 119
120 120 """
121 121 self._locking_url = locking_url
122 122 self._config = settings
123 123
124 124 objectstore_url = self.get_conf('archive_cache.objectstore.url')
125 125 self._storage_path = objectstore_url # common path for all from BaseCache
126 126
127 127 self._shard_count = int(self.get_conf('archive_cache.objectstore.bucket_shards', pop=True))
128 128 if self._shard_count < 1:
129 129 raise ValueError('cache_shards must be 1 or more')
130 130
131 131 self._bucket = settings.pop('archive_cache.objectstore.bucket')
132 132 if not self._bucket:
133 133 raise ValueError('archive_cache.objectstore.bucket needs to have a value')
134 134
135 135 self._eviction_policy = self.get_conf('archive_cache.objectstore.eviction_policy', pop=True)
136 136 self._cache_size_limit = self.gb_to_bytes(int(self.get_conf('archive_cache.objectstore.cache_size_gb')))
137 137
138 138 self.retry = str2bool(self.get_conf('archive_cache.objectstore.retry', pop=True))
139 139 self.retry_attempts = int(self.get_conf('archive_cache.objectstore.retry_attempts', pop=True))
140 140 self.retry_backoff = int(self.get_conf('archive_cache.objectstore.retry_backoff', pop=True))
141 141
142 142 endpoint_url = settings.pop('archive_cache.objectstore.url')
143 143 key = settings.pop('archive_cache.objectstore.key')
144 144 secret = settings.pop('archive_cache.objectstore.secret')
145 145 region = settings.pop('archive_cache.objectstore.region')
146 146
147 147 log.debug('Initializing %s archival cache instance', self)
148 148
149 149 fs = fsspec.filesystem(
150 150 's3', anon=False, endpoint_url=endpoint_url, key=key, secret=secret, client_kwargs={'region_name': region}
151 151 )
152 152
153 153 # init main bucket
154 154 if not fs.exists(self._bucket):
155 155 fs.mkdir(self._bucket)
156 156
157 157 self._shards = tuple(
158 158 self.shard_cls(
159 159 index=num,
160 160 bucket=self._bucket,
161 161 bucket_folder=self.shard_name.format(num),
162 162 fs=fs,
163 163 **settings,
164 164 )
165 165 for num in range(self._shard_count)
166 166 )
167 167 self._hash = self._shards[0].hash
168 168
169 169 def _get_size(self, shard, archive_path):
170 170 return shard.fs.info(archive_path)['size']
171 171
172 172 def set_presigned_url_expiry(self, val: int) -> None:
173 173 self.presigned_url_expires = val
@@ -1,372 +1,373 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18 """
19 19 Celery loader, run with::
20 20
21 21 celery worker \
22 22 --task-events \
23 23 --beat \
24 24 --autoscale=20,2 \
25 25 --max-tasks-per-child 1 \
26 26 --app rhodecode.lib.celerylib.loader \
27 27 --scheduler rhodecode.lib.celerylib.scheduler.RcScheduler \
28 28 --loglevel DEBUG --ini=.dev/dev.ini
29 29 """
30 from rhodecode.config.patches import inspect_getargspec, inspect_formatargspec
31 inspect_getargspec()
32 inspect_formatargspec()
30 from rhodecode.config import patches
31 patches.inspect_getargspec()
32 patches.inspect_formatargspec()
33 33 # python3.11 inspect patches for backward compat on `paste` code
34 patches.repoze_sendmail_lf_fix()
34 35
35 36 import sys
36 37 import logging
37 38 import importlib
38 39
39 40 import click
40 41 from celery import Celery
41 42 from celery import signals
42 43 from celery import Task
43 44 from celery import exceptions # noqa
44 45
45 46 import rhodecode
46 47
47 48 from rhodecode.lib.statsd_client import StatsdClient
48 49 from rhodecode.lib.celerylib.utils import parse_ini_vars, ping_db
49 50 from rhodecode.lib.ext_json import json
50 51 from rhodecode.lib.pyramid_utils import bootstrap, setup_logging
51 52 from rhodecode.lib.utils2 import str2bool
52 53 from rhodecode.model import meta
53 54
54 55 log = logging.getLogger('celery.rhodecode.loader')
55 56
56 57
57 58 imports = ['rhodecode.lib.celerylib.tasks']
58 59
59 60 try:
60 61 # try if we have EE tasks available
61 62 importlib.import_module('rc_ee')
62 63 imports.append('rc_ee.lib.celerylib.tasks')
63 64 except ImportError:
64 65 pass
65 66
66 67
67 68 base_celery_config = {
68 69 'result_backend': 'rpc://',
69 70 'result_expires': 60 * 60 * 24,
70 71 'result_persistent': True,
71 72 'imports': imports,
72 73 'worker_max_tasks_per_child': 100,
73 74 'worker_hijack_root_logger': False,
74 75 'worker_prefetch_multiplier': 1,
75 76 'task_serializer': 'json',
76 77 'accept_content': ['json', 'msgpack'],
77 78 'result_serializer': 'json',
78 79 'result_accept_content': ['json', 'msgpack'],
79 80
80 81 'broker_connection_retry_on_startup': True,
81 82 'database_table_names': {
82 83 'task': 'beat_taskmeta',
83 84 'group': 'beat_groupmeta',
84 85 }
85 86 }
86 87
87 88
88 89 preload_option_ini = click.Option(
89 90 ('--ini',),
90 91 help='Path to ini configuration file.'
91 92 )
92 93
93 94 preload_option_ini_var = click.Option(
94 95 ('--ini-var',),
95 96 help='Comma separated list of key=value to pass to ini.'
96 97 )
97 98
98 99
99 100 def get_logger(obj):
100 101 custom_log = logging.getLogger(
101 102 'rhodecode.task.{}'.format(obj.__class__.__name__))
102 103
103 104 if rhodecode.CELERY_ENABLED:
104 105 try:
105 106 custom_log = obj.get_logger()
106 107 except Exception:
107 108 pass
108 109
109 110 return custom_log
110 111
111 112
112 113 # init main celery app
113 114 celery_app = Celery()
114 115 celery_app.user_options['preload'].add(preload_option_ini)
115 116 celery_app.user_options['preload'].add(preload_option_ini_var)
116 117
117 118
118 119 @signals.setup_logging.connect
119 120 def setup_logging_callback(**kwargs):
120 121
121 122 if 'RC_INI_FILE' in celery_app.conf:
122 123 ini_file = celery_app.conf['RC_INI_FILE']
123 124 else:
124 125 ini_file = celery_app.user_options['RC_INI_FILE']
125 126
126 127 setup_logging(ini_file)
127 128
128 129
129 130 @signals.user_preload_options.connect
130 131 def on_preload_parsed(options, **kwargs):
131 132
132 133 ini_file = options['ini']
133 134 ini_vars = options['ini_var']
134 135
135 136 if ini_file is None:
136 137 print('You must provide the --ini argument to start celery')
137 138 exit(-1)
138 139
139 140 options = None
140 141 if ini_vars is not None:
141 142 options = parse_ini_vars(ini_vars)
142 143
143 144 celery_app.conf['RC_INI_FILE'] = ini_file
144 145 celery_app.user_options['RC_INI_FILE'] = ini_file
145 146
146 147 celery_app.conf['RC_INI_OPTIONS'] = options
147 148 celery_app.user_options['RC_INI_OPTIONS'] = options
148 149
149 150 setup_logging(ini_file)
150 151
151 152
152 153 def _init_celery(app_type=''):
153 154 from rhodecode.config.middleware import get_celery_config
154 155
155 156 log.debug('Bootstrapping RhodeCode application for %s...', app_type)
156 157
157 158 ini_file = celery_app.conf['RC_INI_FILE']
158 159 options = celery_app.conf['RC_INI_OPTIONS']
159 160
160 161 env = None
161 162 try:
162 163 env = bootstrap(ini_file, options=options)
163 164 except Exception:
164 165 log.exception('Failed to bootstrap RhodeCode APP. '
165 166 'Probably there is another error present that prevents from running pyramid app')
166 167
167 168 if not env:
168 169 # we use sys.exit here since we need to signal app startup failure for docker to restart the container and re-try
169 170 sys.exit(1)
170 171
171 172 log.debug('Got Pyramid ENV: %s', env)
172 173
173 174 settings = env['registry'].settings
174 175 celery_settings = get_celery_config(settings)
175 176
176 177 # init and bootstrap StatsdClient
177 178 StatsdClient.setup(settings)
178 179
179 180 setup_celery_app(
180 181 app=env['app'], root=env['root'], request=env['request'],
181 182 registry=env['registry'], closer=env['closer'],
182 183 celery_settings=celery_settings)
183 184
184 185
185 186 @signals.celeryd_init.connect
186 187 def on_celeryd_init(sender=None, conf=None, **kwargs):
187 188 _init_celery('celery worker')
188 189
189 190 # fix the global flag even if it's disabled via .ini file because this
190 191 # is a worker code that doesn't need this to be disabled.
191 192 rhodecode.CELERY_ENABLED = True
192 193
193 194
194 195 @signals.beat_init.connect
195 196 def on_beat_init(sender=None, conf=None, **kwargs):
196 197 _init_celery('celery beat')
197 198
198 199
199 200 @signals.task_prerun.connect
200 201 def task_prerun_signal(task_id, task, args, **kwargs):
201 202 ping_db()
202 203 statsd = StatsdClient.statsd
203 204
204 205 if statsd:
205 206 task_repr = getattr(task, 'name', task)
206 207 statsd.incr('rhodecode_celery_task_total', tags=[
207 208 f'task:{task_repr}',
208 209 'mode:async'
209 210 ])
210 211
211 212
212 213 @signals.task_success.connect
213 214 def task_success_signal(result, **kwargs):
214 215 meta.Session.commit()
215 216 closer = celery_app.conf['PYRAMID_CLOSER']
216 217 if closer:
217 218 closer()
218 219
219 220
220 221 @signals.task_retry.connect
221 222 def task_retry_signal(
222 223 request, reason, einfo, **kwargs):
223 224 meta.Session.remove()
224 225 closer = celery_app.conf['PYRAMID_CLOSER']
225 226 if closer:
226 227 closer()
227 228
228 229
229 230 @signals.task_failure.connect
230 231 def task_failure_signal(
231 232 task_id, exception, args, kwargs, traceback, einfo, **kargs):
232 233
233 234 log.error('Task: %s failed !! exc_info: %s', task_id, einfo)
234 235 from rhodecode.lib.exc_tracking import store_exception
235 236 from rhodecode.lib.statsd_client import StatsdClient
236 237
237 238 meta.Session.remove()
238 239
239 240 # simulate sys.exc_info()
240 241 exc_info = (einfo.type, einfo.exception, einfo.tb)
241 242 store_exception(id(exc_info), exc_info, prefix='rhodecode-celery')
242 243 statsd = StatsdClient.statsd
243 244 if statsd:
244 245 exc_type = "{}.{}".format(einfo.__class__.__module__, einfo.__class__.__name__)
245 246 statsd.incr('rhodecode_exception_total',
246 247 tags=["exc_source:celery", "type:{}".format(exc_type)])
247 248
248 249 closer = celery_app.conf['PYRAMID_CLOSER']
249 250 if closer:
250 251 closer()
251 252
252 253
253 254 @signals.task_revoked.connect
254 255 def task_revoked_signal(
255 256 request, terminated, signum, expired, **kwargs):
256 257 closer = celery_app.conf['PYRAMID_CLOSER']
257 258 if closer:
258 259 closer()
259 260
260 261
261 262 class UNSET(object):
262 263 pass
263 264
264 265
265 266 _unset = UNSET()
266 267
267 268
268 269 def set_celery_conf(app=_unset, root=_unset, request=_unset, registry=_unset, closer=_unset):
269 270
270 271 if request is not UNSET:
271 272 celery_app.conf.update({'PYRAMID_REQUEST': request})
272 273
273 274 if registry is not UNSET:
274 275 celery_app.conf.update({'PYRAMID_REGISTRY': registry})
275 276
276 277
277 278 def setup_celery_app(app, root, request, registry, closer, celery_settings):
278 279 log.debug('Got custom celery conf: %s', celery_settings)
279 280 celery_config = base_celery_config
280 281 celery_config.update({
281 282 # store celerybeat scheduler db where the .ini file is
282 283 'beat_schedule_filename': registry.settings['celerybeat-schedule.path'],
283 284 })
284 285
285 286 celery_config.update(celery_settings)
286 287 celery_app.config_from_object(celery_config)
287 288
288 289 celery_app.conf.update({'PYRAMID_APP': app})
289 290 celery_app.conf.update({'PYRAMID_ROOT': root})
290 291 celery_app.conf.update({'PYRAMID_REQUEST': request})
291 292 celery_app.conf.update({'PYRAMID_REGISTRY': registry})
292 293 celery_app.conf.update({'PYRAMID_CLOSER': closer})
293 294
294 295
295 296 def configure_celery(config, celery_settings):
296 297 """
297 298 Helper that is called from our application creation logic. It gives
298 299 connection info into running webapp and allows execution of tasks from
299 300 RhodeCode itself
300 301 """
301 302 # store some globals into rhodecode
302 303 rhodecode.CELERY_ENABLED = str2bool(
303 304 config.registry.settings.get('use_celery'))
304 305 if rhodecode.CELERY_ENABLED:
305 306 log.info('Configuring celery based on `%s` settings', celery_settings)
306 307 setup_celery_app(
307 308 app=None, root=None, request=None, registry=config.registry,
308 309 closer=None, celery_settings=celery_settings)
309 310
310 311
311 312 def maybe_prepare_env(req):
312 313 environ = {}
313 314 try:
314 315 environ.update({
315 316 'PATH_INFO': req.environ['PATH_INFO'],
316 317 'SCRIPT_NAME': req.environ['SCRIPT_NAME'],
317 318 'HTTP_HOST': req.environ.get('HTTP_HOST', req.environ['SERVER_NAME']),
318 319 'SERVER_NAME': req.environ['SERVER_NAME'],
319 320 'SERVER_PORT': req.environ['SERVER_PORT'],
320 321 'wsgi.url_scheme': req.environ['wsgi.url_scheme'],
321 322 })
322 323 except Exception:
323 324 pass
324 325
325 326 return environ
326 327
327 328
328 329 class RequestContextTask(Task):
329 330 """
330 331 This is a celery task which will create a rhodecode app instance context
331 332 for the task, patch pyramid with the original request
332 333 that created the task and also add the user to the context.
333 334 """
334 335
335 336 def apply_async(self, args=None, kwargs=None, task_id=None, producer=None,
336 337 link=None, link_error=None, shadow=None, **options):
337 338 """ queue the job to run (we are in web request context here) """
338 339 from rhodecode.lib.base import get_ip_addr
339 340
340 341 req = self.app.conf['PYRAMID_REQUEST']
341 342 if not req:
342 343 raise ValueError('celery_app.conf is having empty PYRAMID_REQUEST key')
343 344
344 345 log.debug('Running Task with class: %s. Request Class: %s',
345 346 self.__class__, req.__class__)
346 347
347 348 user_id = 0
348 349
349 350 # web case
350 351 if hasattr(req, 'user'):
351 352 user_id = req.user.user_id
352 353
353 354 # api case
354 355 elif hasattr(req, 'rpc_user'):
355 356 user_id = req.rpc_user.user_id
356 357
357 358 # we hook into kwargs since it is the only way to pass our data to
358 359 # the celery worker
359 360 environ = maybe_prepare_env(req)
360 361 options['headers'] = options.get('headers', {})
361 362 options['headers'].update({
362 363 'rhodecode_proxy_data': {
363 364 'environ': environ,
364 365 'auth_user': {
365 366 'ip_addr': get_ip_addr(req.environ),
366 367 'user_id': user_id
367 368 },
368 369 }
369 370 })
370 371
371 372 return super(RequestContextTask, self).apply_async(
372 373 args, kwargs, task_id, producer, link, link_error, shadow, **options)
@@ -1,454 +1,455 b''
1 # Copyright (C) 2012-2023 RhodeCode GmbH
1 # Copyright (C) 2012-2024 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 RhodeCode task modules, containing all task that suppose to be run
21 21 by celery daemon
22 22 """
23 23
24 24 import os
25 25 import time
26 26
27 27 from pyramid_mailer.mailer import Mailer
28 28 from pyramid_mailer.message import Message
29 29 from email.utils import formatdate
30 30
31 31 import rhodecode
32 32 from rhodecode.lib import audit_logger
33 33 from rhodecode.lib.celerylib import get_logger, async_task, RequestContextTask, run_task
34 34 from rhodecode.lib import hooks_base
35 35 from rhodecode.lib.utils import adopt_for_celery
36 36 from rhodecode.lib.utils2 import safe_int, str2bool, aslist
37 37 from rhodecode.lib.statsd_client import StatsdClient
38 38 from rhodecode.model.db import (
39 39 true, null, Session, IntegrityError, Repository, RepoGroup, User)
40 40 from rhodecode.model.permission import PermissionModel
41 41
42 42
43 43 @async_task(ignore_result=True, base=RequestContextTask)
44 44 def send_email(recipients, subject, body='', html_body='', email_config=None,
45 45 extra_headers=None):
46 46 """
47 47 Sends an email with defined parameters from the .ini files.
48 48
49 49 :param recipients: list of recipients, it this is empty the defined email
50 50 address from field 'email_to' is used instead
51 51 :param subject: subject of the mail
52 52 :param body: body of the mail
53 53 :param html_body: html version of body
54 54 :param email_config: specify custom configuration for mailer
55 55 :param extra_headers: specify custom headers
56 56 """
57 57 log = get_logger(send_email)
58 58
59 59 email_config = email_config or rhodecode.CONFIG
60 60
61 61 mail_server = email_config.get('smtp_server') or None
62 62 if mail_server is None:
63 63 log.error("SMTP server information missing. Sending email failed. "
64 64 "Make sure that `smtp_server` variable is configured "
65 65 "inside the .ini file")
66 66 return False
67
68 subject = "%s %s" % (email_config.get('email_prefix', ''), subject)
67 conf_prefix = email_config.get('email_prefix', None)
68 prefix = f'{conf_prefix} ' if conf_prefix else ''
69 subject = f"{prefix}{subject}"
69 70
70 71 if recipients:
71 72 if isinstance(recipients, str):
72 73 recipients = recipients.split(',')
73 74 else:
74 75 # if recipients are not defined we send to email_config + all admins
75 76 admins = []
76 77 for u in User.query().filter(User.admin == true()).all():
77 78 if u.email:
78 79 admins.append(u.email)
79 80 recipients = []
80 81 config_email = email_config.get('email_to')
81 82 if config_email:
82 83 recipients += [config_email]
83 84 recipients += admins
84 85
85 86 # translate our LEGACY config into the one that pyramid_mailer supports
86 87 email_conf = dict(
87 88 host=mail_server,
88 89 port=email_config.get('smtp_port', 25),
89 username=email_config.get('smtp_username'),
90 password=email_config.get('smtp_password'),
90 username=email_config.get('smtp_username', None),
91 password=email_config.get('smtp_password', None),
91 92
92 93 tls=str2bool(email_config.get('smtp_use_tls')),
93 94 ssl=str2bool(email_config.get('smtp_use_ssl')),
94 95
95 96 # SSL key file
96 97 # keyfile='',
97 98
98 99 # SSL certificate file
99 100 # certfile='',
100 101
101 102 # Location of maildir
102 103 # queue_path='',
103 104
104 105 default_sender=email_config.get('app_email_from', 'RhodeCode-noreply@rhodecode.com'),
105 106
106 107 debug=str2bool(email_config.get('smtp_debug')),
107 108 # /usr/sbin/sendmail Sendmail executable
108 109 # sendmail_app='',
109 110
110 111 # {sendmail_app} -t -i -f {sender} Template for sendmail execution
111 112 # sendmail_template='',
112 113 )
113 114
114 115 if extra_headers is None:
115 116 extra_headers = {}
116 117
117 118 extra_headers.setdefault('Date', formatdate(time.time()))
118 119
119 120 if 'thread_ids' in extra_headers:
120 121 thread_ids = extra_headers.pop('thread_ids')
121 122 extra_headers['References'] = ' '.join('<{}>'.format(t) for t in thread_ids)
122 123
123 124 try:
124 125 mailer = Mailer(**email_conf)
125 126
126 127 message = Message(subject=subject,
127 128 sender=email_conf['default_sender'],
128 129 recipients=recipients,
129 130 body=body, html=html_body,
130 131 extra_headers=extra_headers)
131 132 mailer.send_immediately(message)
132 133 statsd = StatsdClient.statsd
133 134 if statsd:
134 135 statsd.incr('rhodecode_email_sent_total')
135 136
136 137 except Exception:
137 138 log.exception('Mail sending failed')
138 139 return False
139 140 return True
140 141
141 142
142 143 @async_task(ignore_result=True, base=RequestContextTask)
143 144 def create_repo(form_data, cur_user):
144 145 from rhodecode.model.repo import RepoModel
145 146 from rhodecode.model.user import UserModel
146 147 from rhodecode.model.scm import ScmModel
147 148 from rhodecode.model.settings import SettingsModel
148 149
149 150 log = get_logger(create_repo)
150 151
151 152 cur_user = UserModel()._get_user(cur_user)
152 153 owner = cur_user
153 154
154 155 repo_name = form_data['repo_name']
155 156 repo_name_full = form_data['repo_name_full']
156 157 repo_type = form_data['repo_type']
157 158 description = form_data['repo_description']
158 159 private = form_data['repo_private']
159 160 clone_uri = form_data.get('clone_uri')
160 161 repo_group = safe_int(form_data['repo_group'])
161 162 copy_fork_permissions = form_data.get('copy_permissions')
162 163 copy_group_permissions = form_data.get('repo_copy_permissions')
163 164 fork_of = form_data.get('fork_parent_id')
164 165 state = form_data.get('repo_state', Repository.STATE_PENDING)
165 166
166 167 # repo creation defaults, private and repo_type are filled in form
167 168 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
168 169 enable_statistics = form_data.get(
169 170 'enable_statistics', defs.get('repo_enable_statistics'))
170 171 enable_locking = form_data.get(
171 172 'enable_locking', defs.get('repo_enable_locking'))
172 173 enable_downloads = form_data.get(
173 174 'enable_downloads', defs.get('repo_enable_downloads'))
174 175
175 176 # set landing rev based on default branches for SCM
176 177 landing_ref, _label = ScmModel.backend_landing_ref(repo_type)
177 178
178 179 try:
179 180 RepoModel()._create_repo(
180 181 repo_name=repo_name_full,
181 182 repo_type=repo_type,
182 183 description=description,
183 184 owner=owner,
184 185 private=private,
185 186 clone_uri=clone_uri,
186 187 repo_group=repo_group,
187 188 landing_rev=landing_ref,
188 189 fork_of=fork_of,
189 190 copy_fork_permissions=copy_fork_permissions,
190 191 copy_group_permissions=copy_group_permissions,
191 192 enable_statistics=enable_statistics,
192 193 enable_locking=enable_locking,
193 194 enable_downloads=enable_downloads,
194 195 state=state
195 196 )
196 197
197 198 Session().commit()
198 199
199 200 # now create this repo on Filesystem
200 201 RepoModel()._create_filesystem_repo(
201 202 repo_name=repo_name,
202 203 repo_type=repo_type,
203 204 repo_group=RepoModel()._get_repo_group(repo_group),
204 205 clone_uri=clone_uri,
205 206 )
206 207 repo = Repository.get_by_repo_name(repo_name_full)
207 208 hooks_base.create_repository(created_by=owner.username, **repo.get_dict())
208 209
209 210 # update repo commit caches initially
210 repo.update_commit_cache()
211 repo.update_commit_cache(recursive=False)
211 212
212 213 # set new created state
213 214 repo.set_state(Repository.STATE_CREATED)
214 215 repo_id = repo.repo_id
215 216 repo_data = repo.get_api_data()
216 217
217 218 audit_logger.store(
218 219 'repo.create', action_data={'data': repo_data},
219 220 user=cur_user,
220 221 repo=audit_logger.RepoWrap(repo_name=repo_name, repo_id=repo_id))
221 222
222 223 Session().commit()
223 224
224 225 PermissionModel().trigger_permission_flush()
225 226
226 227 except Exception as e:
227 228 log.warning('Exception occurred when creating repository, '
228 229 'doing cleanup...', exc_info=True)
229 230 if isinstance(e, IntegrityError):
230 231 Session().rollback()
231 232
232 233 # rollback things manually !
233 234 repo = Repository.get_by_repo_name(repo_name_full)
234 235 if repo:
235 236 Repository.delete(repo.repo_id)
236 237 Session().commit()
237 238 RepoModel()._delete_filesystem_repo(repo)
238 239 log.info('Cleanup of repo %s finished', repo_name_full)
239 240 raise
240 241
241 242 return True
242 243
243 244
244 245 @async_task(ignore_result=True, base=RequestContextTask)
245 246 def create_repo_fork(form_data, cur_user):
246 247 """
247 248 Creates a fork of repository using internal VCS methods
248 249 """
249 250 from rhodecode.model.repo import RepoModel
250 251 from rhodecode.model.user import UserModel
251 252
252 253 log = get_logger(create_repo_fork)
253 254
254 255 cur_user = UserModel()._get_user(cur_user)
255 256 owner = cur_user
256 257
257 258 repo_name = form_data['repo_name'] # fork in this case
258 259 repo_name_full = form_data['repo_name_full']
259 260 repo_type = form_data['repo_type']
260 261 description = form_data['description']
261 262 private = form_data['private']
262 263 clone_uri = form_data.get('clone_uri')
263 264 repo_group = safe_int(form_data['repo_group'])
264 265 landing_ref = form_data['landing_rev']
265 266 copy_fork_permissions = form_data.get('copy_permissions')
266 267 fork_id = safe_int(form_data.get('fork_parent_id'))
267 268
268 269 try:
269 270 fork_of = RepoModel()._get_repo(fork_id)
270 271 RepoModel()._create_repo(
271 272 repo_name=repo_name_full,
272 273 repo_type=repo_type,
273 274 description=description,
274 275 owner=owner,
275 276 private=private,
276 277 clone_uri=clone_uri,
277 278 repo_group=repo_group,
278 279 landing_rev=landing_ref,
279 280 fork_of=fork_of,
280 281 copy_fork_permissions=copy_fork_permissions
281 282 )
282 283
283 284 Session().commit()
284 285
285 286 base_path = Repository.base_path()
286 287 source_repo_path = os.path.join(base_path, fork_of.repo_name)
287 288
288 289 # now create this repo on Filesystem
289 290 RepoModel()._create_filesystem_repo(
290 291 repo_name=repo_name,
291 292 repo_type=repo_type,
292 293 repo_group=RepoModel()._get_repo_group(repo_group),
293 294 clone_uri=source_repo_path,
294 295 )
295 296 repo = Repository.get_by_repo_name(repo_name_full)
296 297 hooks_base.create_repository(created_by=owner.username, **repo.get_dict())
297 298
298 299 # update repo commit caches initially
299 300 config = repo._config
300 301 config.set('extensions', 'largefiles', '')
301 repo.update_commit_cache(config=config)
302 repo.update_commit_cache(config=config, recursive=False)
302 303
303 304 # set new created state
304 305 repo.set_state(Repository.STATE_CREATED)
305 306
306 307 repo_id = repo.repo_id
307 308 repo_data = repo.get_api_data()
308 309 audit_logger.store(
309 310 'repo.fork', action_data={'data': repo_data},
310 311 user=cur_user,
311 312 repo=audit_logger.RepoWrap(repo_name=repo_name, repo_id=repo_id))
312 313
313 314 Session().commit()
314 315 except Exception as e:
315 316 log.warning('Exception occurred when forking repository, '
316 317 'doing cleanup...', exc_info=True)
317 318 if isinstance(e, IntegrityError):
318 319 Session().rollback()
319 320
320 321 # rollback things manually !
321 322 repo = Repository.get_by_repo_name(repo_name_full)
322 323 if repo:
323 324 Repository.delete(repo.repo_id)
324 325 Session().commit()
325 326 RepoModel()._delete_filesystem_repo(repo)
326 327 log.info('Cleanup of repo %s finished', repo_name_full)
327 328 raise
328 329
329 330 return True
330 331
331 332
332 333 @async_task(ignore_result=True, base=RequestContextTask)
333 334 def repo_maintenance(repoid):
334 335 from rhodecode.lib import repo_maintenance as repo_maintenance_lib
335 336 log = get_logger(repo_maintenance)
336 337 repo = Repository.get_by_id_or_repo_name(repoid)
337 338 if repo:
338 339 maintenance = repo_maintenance_lib.RepoMaintenance()
339 340 tasks = maintenance.get_tasks_for_repo(repo)
340 341 log.debug('Executing %s tasks on repo `%s`', tasks, repoid)
341 342 executed_types = maintenance.execute(repo)
342 343 log.debug('Got execution results %s', executed_types)
343 344 else:
344 345 log.debug('Repo `%s` not found or without a clone_url', repoid)
345 346
346 347
347 348 @async_task(ignore_result=True, base=RequestContextTask)
348 349 def check_for_update(send_email_notification=True, email_recipients=None):
349 350 from rhodecode.model.update import UpdateModel
350 351 from rhodecode.model.notification import EmailNotificationModel
351 352
352 353 log = get_logger(check_for_update)
353 354 update_url = UpdateModel().get_update_url()
354 355 cur_ver = rhodecode.__version__
355 356
356 357 try:
357 358 data = UpdateModel().get_update_data(update_url)
358 359
359 360 current_ver = UpdateModel().get_stored_version(fallback=cur_ver)
360 361 latest_ver = data['versions'][0]['version']
361 362 UpdateModel().store_version(latest_ver)
362 363
363 364 if send_email_notification:
364 365 log.debug('Send email notification is enabled. '
365 366 'Current RhodeCode version: %s, latest known: %s', current_ver, latest_ver)
366 367 if UpdateModel().is_outdated(current_ver, latest_ver):
367 368
368 369 email_kwargs = {
369 370 'current_ver': current_ver,
370 371 'latest_ver': latest_ver,
371 372 }
372 373
373 374 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
374 375 EmailNotificationModel.TYPE_UPDATE_AVAILABLE, **email_kwargs)
375 376
376 377 email_recipients = aslist(email_recipients, sep=',') or \
377 378 [user.email for user in User.get_all_super_admins()]
378 379 run_task(send_email, email_recipients, subject,
379 380 email_body_plaintext, email_body)
380 381
381 382 except Exception:
382 383 log.exception('Failed to check for update')
383 384 raise
384 385
385 386
386 387 def sync_last_update_for_objects(*args, **kwargs):
387 388 skip_repos = kwargs.get('skip_repos')
388 389 if not skip_repos:
389 390 repos = Repository.query() \
390 391 .order_by(Repository.group_id.asc())
391 392
392 393 for repo in repos:
393 repo.update_commit_cache()
394 repo.update_commit_cache(recursive=False)
394 395
395 396 skip_groups = kwargs.get('skip_groups')
396 397 if not skip_groups:
397 398 repo_groups = RepoGroup.query() \
398 399 .filter(RepoGroup.group_parent_id == null())
399 400
400 401 for root_gr in repo_groups:
401 402 for repo_gr in reversed(root_gr.recursive_groups()):
402 403 repo_gr.update_commit_cache()
403 404
404 405
405 406 @async_task(ignore_result=True, base=RequestContextTask)
406 407 def test_celery_exception(msg):
407 408 raise Exception(f'Test exception: {msg}')
408 409
409 410
410 411 @async_task(ignore_result=True, base=RequestContextTask)
411 412 def sync_last_update(*args, **kwargs):
412 413 sync_last_update_for_objects(*args, **kwargs)
413 414
414 415
415 416 @async_task(ignore_result=False)
416 417 def beat_check(*args, **kwargs):
417 418 log = get_logger(beat_check)
418 419 log.info('%r: Got args: %r and kwargs %r', beat_check, args, kwargs)
419 420 return time.time()
420 421
421 422
422 423 @async_task
423 424 @adopt_for_celery
424 425 def repo_size(extras):
425 426 from rhodecode.lib.hooks_base import repo_size
426 427 return repo_size(extras)
427 428
428 429
429 430 @async_task
430 431 @adopt_for_celery
431 432 def pre_pull(extras):
432 433 from rhodecode.lib.hooks_base import pre_pull
433 434 return pre_pull(extras)
434 435
435 436
436 437 @async_task
437 438 @adopt_for_celery
438 439 def post_pull(extras):
439 440 from rhodecode.lib.hooks_base import post_pull
440 441 return post_pull(extras)
441 442
442 443
443 444 @async_task
444 445 @adopt_for_celery
445 446 def pre_push(extras):
446 447 from rhodecode.lib.hooks_base import pre_push
447 448 return pre_push(extras)
448 449
449 450
450 451 @async_task
451 452 @adopt_for_celery
452 453 def post_push(extras):
453 454 from rhodecode.lib.hooks_base import post_push
454 455 return post_push(extras)
@@ -1,679 +1,678 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 Database creation, and setup module for RhodeCode Enterprise. Used for creation
21 21 of database as well as for migration operations
22 22 """
23 23
24 24 import os
25 25 import sys
26 26 import time
27 27 import uuid
28 28 import logging
29 29 import getpass
30 30 from os.path import dirname as dn, join as jn
31 31
32 32 from sqlalchemy.engine import create_engine
33 33
34 34 from rhodecode import __dbversion__
35 35 from rhodecode.model import init_model
36 36 from rhodecode.model.user import UserModel
37 37 from rhodecode.model.db import (
38 38 User, Permission, RhodeCodeUi, RhodeCodeSetting, UserToPerm,
39 39 DbMigrateVersion, RepoGroup, UserRepoGroupToPerm, CacheKey, Repository)
40 40 from rhodecode.model.meta import Session, Base
41 41 from rhodecode.model.permission import PermissionModel
42 42 from rhodecode.model.repo import RepoModel
43 43 from rhodecode.model.repo_group import RepoGroupModel
44 44 from rhodecode.model.settings import SettingsModel
45 45
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 def notify(msg):
51 51 """
52 52 Notification for migrations messages
53 53 """
54 54 ml = len(msg) + (4 * 2)
55 55 print((('\n%s\n*** %s ***\n%s' % ('*' * ml, msg, '*' * ml)).upper()))
56 56
57 57
58 58 class DbManage(object):
59 59
60 60 def __init__(self, log_sql, dbconf, root, tests=False,
61 61 SESSION=None, cli_args=None, enc_key=b''):
62 62
63 63 self.dbname = dbconf.split('/')[-1]
64 64 self.tests = tests
65 65 self.root = root
66 66 self.dburi = dbconf
67 67 self.log_sql = log_sql
68 68 self.cli_args = cli_args or {}
69 69 self.sa = None
70 70 self.engine = None
71 71 self.enc_key = enc_key
72 72 # sets .sa .engine
73 73 self.init_db(SESSION=SESSION)
74 74
75 75 self.ask_ok = self.get_ask_ok_func(self.cli_args.get('force_ask'))
76 76
77 77 def db_exists(self):
78 78 if not self.sa:
79 79 self.init_db()
80 80 try:
81 81 self.sa.query(RhodeCodeUi)\
82 82 .filter(RhodeCodeUi.ui_key == '/')\
83 83 .scalar()
84 84 return True
85 85 except Exception:
86 86 return False
87 87 finally:
88 88 self.sa.rollback()
89 89
90 90 def get_ask_ok_func(self, param):
91 91 if param not in [None]:
92 92 # return a function lambda that has a default set to param
93 93 return lambda *args, **kwargs: param
94 94 else:
95 95 from rhodecode.lib.utils import ask_ok
96 96 return ask_ok
97 97
98 98 def init_db(self, SESSION=None):
99 99
100 100 if SESSION:
101 101 self.sa = SESSION
102 102 self.engine = SESSION.bind
103 103 else:
104 104 # init new sessions
105 105 engine = create_engine(self.dburi, echo=self.log_sql)
106 106 init_model(engine, encryption_key=self.enc_key)
107 107 self.sa = Session()
108 108 self.engine = engine
109 109
110 110 def create_tables(self, override=False):
111 111 """
112 112 Create a auth database
113 113 """
114 114
115 115 log.info("Existing database with the same name is going to be destroyed.")
116 116 log.info("Setup command will run DROP ALL command on that database.")
117 117 engine = self.engine
118 118
119 119 if self.tests:
120 120 destroy = True
121 121 else:
122 122 destroy = self.ask_ok('Are you sure that you want to destroy the old database? [y/n]')
123 123 if not destroy:
124 124 log.info('db tables bootstrap: Nothing done.')
125 125 sys.exit(0)
126 126 if destroy:
127 127 Base.metadata.drop_all(bind=engine)
128 128
129 129 checkfirst = not override
130 130 Base.metadata.create_all(bind=engine, checkfirst=checkfirst)
131 131 log.info('Created tables for %s', self.dbname)
132 132
133 133 def set_db_version(self):
134 134 ver = DbMigrateVersion()
135 135 ver.version = __dbversion__
136 136 ver.repository_id = 'rhodecode_db_migrations'
137 137 ver.repository_path = 'versions'
138 138 self.sa.add(ver)
139 139 log.info('db version set to: %s', __dbversion__)
140 140
141 141 def run_post_migration_tasks(self):
142 142 """
143 143 Run various tasks before actually doing migrations
144 144 """
145 145 # delete cache keys on each upgrade
146 146 total = CacheKey.query().count()
147 147 log.info("Deleting (%s) cache keys now...", total)
148 148 CacheKey.delete_all_cache()
149 149
150 150 def upgrade(self, version=None):
151 151 """
152 152 Upgrades given database schema to given revision following
153 153 all needed steps, to perform the upgrade
154 154
155 155 """
156 156
157 157 from rhodecode.lib.dbmigrate.migrate.versioning import api
158 158 from rhodecode.lib.dbmigrate.migrate.exceptions import DatabaseNotControlledError
159 159
160 160 if 'sqlite' in self.dburi:
161 161 print(
162 162 '********************** WARNING **********************\n'
163 163 'Make sure your version of sqlite is at least 3.7.X. \n'
164 164 'Earlier versions are known to fail on some migrations\n'
165 165 '*****************************************************\n')
166 166
167 167 upgrade = self.ask_ok(
168 168 'You are about to perform a database upgrade. Make '
169 169 'sure you have backed up your database. '
170 170 'Continue ? [y/n]')
171 171 if not upgrade:
172 172 log.info('No upgrade performed')
173 173 sys.exit(0)
174 174
175 175 repository_path = jn(dn(dn(dn(os.path.realpath(__file__)))),
176 176 'rhodecode/lib/dbmigrate')
177 177 db_uri = self.dburi
178 178
179 179 if version:
180 180 DbMigrateVersion.set_version(version)
181 181
182 182 try:
183 183 curr_version = api.db_version(db_uri, repository_path)
184 184 msg = (f'Found current database db_uri under version '
185 185 f'control with version {curr_version}')
186 186
187 187 except (RuntimeError, DatabaseNotControlledError):
188 188 curr_version = 1
189 189 msg = f'Current database is not under version control. ' \
190 190 f'Setting as version {curr_version}'
191 191 api.version_control(db_uri, repository_path, curr_version)
192 192
193 193 notify(msg)
194 194
195 195 if curr_version == __dbversion__:
196 196 log.info('This database is already at the newest version')
197 197 sys.exit(0)
198 198
199 199 upgrade_steps = list(range(curr_version + 1, __dbversion__ + 1))
200 200 notify(f'attempting to upgrade database from '
201 201 f'version {curr_version} to version {__dbversion__}')
202 202
203 203 # CALL THE PROPER ORDER OF STEPS TO PERFORM FULL UPGRADE
204 204 final_step = 'latest'
205 205 for step in upgrade_steps:
206 206 notify(f'performing upgrade step {step}')
207 207 time.sleep(0.5)
208 208
209 209 api.upgrade(db_uri, repository_path, step)
210 210 self.sa.rollback()
211 211 notify(f'schema upgrade for step {step} completed')
212 212
213 213 final_step = step
214 214
215 215 self.run_post_migration_tasks()
216 216 notify(f'upgrade to version {final_step} successful')
217 217
218 218 def fix_repo_paths(self):
219 219 """
220 220 Fixes an old RhodeCode version path into new one without a '*'
221 221 """
222 222
223 223 paths = self.sa.query(RhodeCodeUi)\
224 224 .filter(RhodeCodeUi.ui_key == '/')\
225 225 .scalar()
226 226
227 227 paths.ui_value = paths.ui_value.replace('*', '')
228 228
229 229 try:
230 230 self.sa.add(paths)
231 231 self.sa.commit()
232 232 except Exception:
233 233 self.sa.rollback()
234 234 raise
235 235
236 236 def fix_default_user(self):
237 237 """
238 238 Fixes an old default user with some 'nicer' default values,
239 239 used mostly for anonymous access
240 240 """
241 241 def_user = self.sa.query(User)\
242 242 .filter(User.username == User.DEFAULT_USER)\
243 243 .one()
244 244
245 245 def_user.name = 'Anonymous'
246 246 def_user.lastname = 'User'
247 247 def_user.email = User.DEFAULT_USER_EMAIL
248 248
249 249 try:
250 250 self.sa.add(def_user)
251 251 self.sa.commit()
252 252 except Exception:
253 253 self.sa.rollback()
254 254 raise
255 255
256 256 def fix_settings(self):
257 257 """
258 258 Fixes rhodecode settings and adds ga_code key for google analytics
259 259 """
260 260
261 261 hgsettings3 = RhodeCodeSetting('ga_code', '')
262 262
263 263 try:
264 264 self.sa.add(hgsettings3)
265 265 self.sa.commit()
266 266 except Exception:
267 267 self.sa.rollback()
268 268 raise
269 269
270 270 def create_admin_and_prompt(self):
271 271
272 272 # defaults
273 273 defaults = self.cli_args
274 274 username = defaults.get('username')
275 275 password = defaults.get('password')
276 276 email = defaults.get('email')
277 277
278 278 if username is None:
279 279 username = input('Specify admin username:')
280 280 if password is None:
281 281 password = self._get_admin_password()
282 282 if not password:
283 283 # second try
284 284 password = self._get_admin_password()
285 285 if not password:
286 286 sys.exit()
287 287 if email is None:
288 288 email = input('Specify admin email:')
289 289 api_key = self.cli_args.get('api_key')
290 290 self.create_user(username, password, email, True,
291 291 strict_creation_check=False,
292 292 api_key=api_key)
293 293
294 294 def _get_admin_password(self):
295 295 password = getpass.getpass('Specify admin password '
296 296 '(min 6 chars):')
297 297 confirm = getpass.getpass('Confirm password:')
298 298
299 299 if password != confirm:
300 300 log.error('passwords mismatch')
301 301 return False
302 302 if len(password) < 6:
303 303 log.error('password is too short - use at least 6 characters')
304 304 return False
305 305
306 306 return password
307 307
308 308 def create_test_admin_and_users(self):
309 309 log.info('creating admin and regular test users')
310 310 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, \
311 311 TEST_USER_ADMIN_PASS, TEST_USER_ADMIN_EMAIL, \
312 312 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, \
313 313 TEST_USER_REGULAR_EMAIL, TEST_USER_REGULAR2_LOGIN, \
314 314 TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR2_EMAIL
315 315
316 316 self.create_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
317 317 TEST_USER_ADMIN_EMAIL, True, api_key=True)
318 318
319 319 self.create_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
320 320 TEST_USER_REGULAR_EMAIL, False, api_key=True)
321 321
322 322 self.create_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS,
323 323 TEST_USER_REGULAR2_EMAIL, False, api_key=True)
324 324
325 325 def create_ui_settings(self, repo_store_path):
326 326 """
327 327 Creates ui settings, fills out hooks
328 328 and disables dotencode
329 329 """
330 330 settings_model = SettingsModel(sa=self.sa)
331 331 from rhodecode.lib.vcs.backends.hg import largefiles_store
332 332 from rhodecode.lib.vcs.backends.git import lfs_store
333 333
334 334 # Build HOOKS
335 335 hooks = [
336 336 (RhodeCodeUi.HOOK_REPO_SIZE, 'python:vcsserver.hooks.repo_size'),
337 337
338 338 # HG
339 339 (RhodeCodeUi.HOOK_PRE_PULL, 'python:vcsserver.hooks.pre_pull'),
340 340 (RhodeCodeUi.HOOK_PULL, 'python:vcsserver.hooks.log_pull_action'),
341 341 (RhodeCodeUi.HOOK_PRE_PUSH, 'python:vcsserver.hooks.pre_push'),
342 342 (RhodeCodeUi.HOOK_PRETX_PUSH, 'python:vcsserver.hooks.pre_push'),
343 343 (RhodeCodeUi.HOOK_PUSH, 'python:vcsserver.hooks.log_push_action'),
344 344 (RhodeCodeUi.HOOK_PUSH_KEY, 'python:vcsserver.hooks.key_push'),
345 345
346 346 ]
347 347
348 348 for key, value in hooks:
349 349 hook_obj = settings_model.get_ui_by_key(key)
350 350 hooks2 = hook_obj if hook_obj else RhodeCodeUi()
351 351 hooks2.ui_section = 'hooks'
352 352 hooks2.ui_key = key
353 353 hooks2.ui_value = value
354 354 self.sa.add(hooks2)
355 355
356 356 # enable largefiles
357 357 largefiles = RhodeCodeUi()
358 358 largefiles.ui_section = 'extensions'
359 359 largefiles.ui_key = 'largefiles'
360 360 largefiles.ui_value = ''
361 361 self.sa.add(largefiles)
362 362
363 363 # set default largefiles cache dir, defaults to
364 364 # /repo_store_location/.cache/largefiles
365 365 largefiles = RhodeCodeUi()
366 366 largefiles.ui_section = 'largefiles'
367 367 largefiles.ui_key = 'usercache'
368 368 largefiles.ui_value = largefiles_store(repo_store_path)
369 369
370 370 self.sa.add(largefiles)
371 371
372 372 # set default lfs cache dir, defaults to
373 373 # /repo_store_location/.cache/lfs_store
374 374 lfsstore = RhodeCodeUi()
375 375 lfsstore.ui_section = 'vcs_git_lfs'
376 376 lfsstore.ui_key = 'store_location'
377 377 lfsstore.ui_value = lfs_store(repo_store_path)
378 378
379 379 self.sa.add(lfsstore)
380 380
381 381 # enable hgevolve disabled by default
382 382 hgevolve = RhodeCodeUi()
383 383 hgevolve.ui_section = 'extensions'
384 384 hgevolve.ui_key = 'evolve'
385 385 hgevolve.ui_value = ''
386 386 hgevolve.ui_active = False
387 387 self.sa.add(hgevolve)
388 388
389 389 hgevolve = RhodeCodeUi()
390 390 hgevolve.ui_section = 'experimental'
391 391 hgevolve.ui_key = 'evolution'
392 392 hgevolve.ui_value = ''
393 393 hgevolve.ui_active = False
394 394 self.sa.add(hgevolve)
395 395
396 396 hgevolve = RhodeCodeUi()
397 397 hgevolve.ui_section = 'experimental'
398 398 hgevolve.ui_key = 'evolution.exchange'
399 399 hgevolve.ui_value = ''
400 400 hgevolve.ui_active = False
401 401 self.sa.add(hgevolve)
402 402
403 403 hgevolve = RhodeCodeUi()
404 404 hgevolve.ui_section = 'extensions'
405 405 hgevolve.ui_key = 'topic'
406 406 hgevolve.ui_value = ''
407 407 hgevolve.ui_active = False
408 408 self.sa.add(hgevolve)
409 409
410 410 # enable hggit disabled by default
411 411 hggit = RhodeCodeUi()
412 412 hggit.ui_section = 'extensions'
413 413 hggit.ui_key = 'hggit'
414 414 hggit.ui_value = ''
415 415 hggit.ui_active = False
416 416 self.sa.add(hggit)
417 417
418 418 # set svn branch defaults
419 419 branches = ["/branches/*", "/trunk"]
420 420 tags = ["/tags/*"]
421 421
422 422 for branch in branches:
423 423 settings_model.create_ui_section_value(
424 424 RhodeCodeUi.SVN_BRANCH_ID, branch)
425 425
426 426 for tag in tags:
427 427 settings_model.create_ui_section_value(RhodeCodeUi.SVN_TAG_ID, tag)
428 428
429 429 def create_auth_plugin_options(self, skip_existing=False):
430 430 """
431 431 Create default auth plugin settings, and make it active
432 432
433 433 :param skip_existing:
434 434 """
435 435 defaults = [
436 436 ('auth_plugins',
437 437 'egg:rhodecode-enterprise-ce#token,egg:rhodecode-enterprise-ce#rhodecode',
438 438 'list'),
439 439
440 440 ('auth_authtoken_enabled',
441 441 'True',
442 442 'bool'),
443 443
444 444 ('auth_rhodecode_enabled',
445 445 'True',
446 446 'bool'),
447 447 ]
448 448 for k, v, t in defaults:
449 449 if (skip_existing and
450 450 SettingsModel().get_setting_by_name(k) is not None):
451 451 log.debug('Skipping option %s', k)
452 452 continue
453 453 setting = RhodeCodeSetting(k, v, t)
454 454 self.sa.add(setting)
455 455
456 456 def create_default_options(self, skip_existing=False):
457 457 """Creates default settings"""
458 458
459 459 for k, v, t in [
460 460 ('default_repo_enable_locking', False, 'bool'),
461 461 ('default_repo_enable_downloads', False, 'bool'),
462 462 ('default_repo_enable_statistics', False, 'bool'),
463 463 ('default_repo_private', False, 'bool'),
464 464 ('default_repo_type', 'hg', 'unicode')]:
465 465
466 466 if (skip_existing and
467 467 SettingsModel().get_setting_by_name(k) is not None):
468 468 log.debug('Skipping option %s', k)
469 469 continue
470 470 setting = RhodeCodeSetting(k, v, t)
471 471 self.sa.add(setting)
472 472
473 473 def fixup_groups(self):
474 474 def_usr = User.get_default_user()
475 475 for g in RepoGroup.query().all():
476 476 g.group_name = g.get_new_name(g.name)
477 477 self.sa.add(g)
478 478 # get default perm
479 479 default = UserRepoGroupToPerm.query()\
480 480 .filter(UserRepoGroupToPerm.group == g)\
481 481 .filter(UserRepoGroupToPerm.user == def_usr)\
482 482 .scalar()
483 483
484 484 if default is None:
485 485 log.debug('missing default permission for group %s adding', g)
486 486 perm_obj = RepoGroupModel()._create_default_perms(g)
487 487 self.sa.add(perm_obj)
488 488
489 489 def reset_permissions(self, username):
490 490 """
491 491 Resets permissions to default state, useful when old systems had
492 492 bad permissions, we must clean them up
493 493
494 494 :param username:
495 495 """
496 496 default_user = User.get_by_username(username)
497 497 if not default_user:
498 498 return
499 499
500 500 u2p = UserToPerm.query()\
501 501 .filter(UserToPerm.user == default_user).all()
502 502 fixed = False
503 503 if len(u2p) != len(Permission.DEFAULT_USER_PERMISSIONS):
504 504 for p in u2p:
505 505 Session().delete(p)
506 506 fixed = True
507 507 self.populate_default_permissions()
508 508 return fixed
509 509
510 510 def config_prompt(self, test_repo_path='', retries=3):
511 511 defaults = self.cli_args
512 512 _path = defaults.get('repos_location')
513 513 if retries == 3:
514 514 log.info('Setting up repositories config')
515 515
516 516 if _path is not None:
517 517 path = _path
518 518 elif not self.tests and not test_repo_path:
519 519 path = input(
520 520 'Enter a valid absolute path to store repositories. '
521 521 'All repositories in that path will be added automatically:'
522 522 )
523 523 else:
524 524 path = test_repo_path
525 525 path_ok = True
526 526
527 527 # check proper dir
528 528 if not os.path.isdir(path):
529 529 path_ok = False
530 530 log.error('Given path %s is not a valid directory', path)
531 531
532 532 elif not os.path.isabs(path):
533 533 path_ok = False
534 534 log.error('Given path %s is not an absolute path', path)
535 535
536 536 # check if path is at least readable.
537 537 if not os.access(path, os.R_OK):
538 538 path_ok = False
539 539 log.error('Given path %s is not readable', path)
540 540
541 541 # check write access, warn user about non writeable paths
542 542 elif not os.access(path, os.W_OK) and path_ok:
543 543 log.warning('No write permission to given path %s', path)
544 544
545 545 q = (f'Given path {path} is not writeable, do you want to '
546 546 f'continue with read only mode ? [y/n]')
547 547 if not self.ask_ok(q):
548 548 log.error('Canceled by user')
549 549 sys.exit(-1)
550 550
551 551 if retries == 0:
552 552 sys.exit('max retries reached')
553 553 if not path_ok:
554 554 retries -= 1
555 555 return self.config_prompt(test_repo_path, retries)
556 556
557 557 real_path = os.path.normpath(os.path.realpath(path))
558 558
559 559 if real_path != os.path.normpath(path):
560 560 q = (f'Path looks like a symlink, RhodeCode Enterprise will store '
561 561 f'given path as {real_path} ? [y/n]')
562 562 if not self.ask_ok(q):
563 563 log.error('Canceled by user')
564 564 sys.exit(-1)
565 565
566 566 return real_path
567 567
568 568 def create_settings(self, path):
569 569
570 570 self.create_ui_settings(path)
571 571
572 572 ui_config = [
573 ('web', 'push_ssl', 'False'),
574 573 ('web', 'allow_archive', 'gz zip bz2'),
575 574 ('web', 'allow_push', '*'),
576 575 ('web', 'baseurl', '/'),
577 576 ('paths', '/', path),
578 577 ('phases', 'publish', 'True')
579 578 ]
580 579 for section, key, value in ui_config:
581 580 ui_conf = RhodeCodeUi()
582 581 setattr(ui_conf, 'ui_section', section)
583 582 setattr(ui_conf, 'ui_key', key)
584 583 setattr(ui_conf, 'ui_value', value)
585 584 self.sa.add(ui_conf)
586 585
587 586 # rhodecode app settings
588 587 settings = [
589 588 ('realm', 'RhodeCode', 'unicode'),
590 589 ('title', '', 'unicode'),
591 590 ('pre_code', '', 'unicode'),
592 591 ('post_code', '', 'unicode'),
593 592
594 593 # Visual
595 594 ('show_public_icon', True, 'bool'),
596 595 ('show_private_icon', True, 'bool'),
597 596 ('stylify_metatags', True, 'bool'),
598 597 ('dashboard_items', 100, 'int'),
599 598 ('admin_grid_items', 25, 'int'),
600 599
601 600 ('markup_renderer', 'markdown', 'unicode'),
602 601
603 602 ('repository_fields', True, 'bool'),
604 603 ('show_version', True, 'bool'),
605 604 ('show_revision_number', True, 'bool'),
606 605 ('show_sha_length', 12, 'int'),
607 606
608 607 ('use_gravatar', False, 'bool'),
609 608 ('gravatar_url', User.DEFAULT_GRAVATAR_URL, 'unicode'),
610 609
611 610 ('clone_uri_tmpl', Repository.DEFAULT_CLONE_URI, 'unicode'),
612 611 ('clone_uri_id_tmpl', Repository.DEFAULT_CLONE_URI_ID, 'unicode'),
613 612 ('clone_uri_ssh_tmpl', Repository.DEFAULT_CLONE_URI_SSH, 'unicode'),
614 613 ('support_url', '', 'unicode'),
615 614 ('update_url', RhodeCodeSetting.DEFAULT_UPDATE_URL, 'unicode'),
616 615
617 616 # VCS Settings
618 617 ('pr_merge_enabled', True, 'bool'),
619 618 ('use_outdated_comments', True, 'bool'),
620 619 ('diff_cache', True, 'bool'),
621 620 ]
622 621
623 622 for key, val, type_ in settings:
624 623 sett = RhodeCodeSetting(key, val, type_)
625 624 self.sa.add(sett)
626 625
627 626 self.create_auth_plugin_options()
628 627 self.create_default_options()
629 628
630 629 log.info('created ui config')
631 630
632 631 def create_user(self, username, password, email='', admin=False,
633 632 strict_creation_check=True, api_key=None):
634 633 log.info('creating user `%s`', username)
635 634 user = UserModel().create_or_update(
636 635 username, password, email, firstname='RhodeCode', lastname='Admin',
637 636 active=True, admin=admin, extern_type="rhodecode",
638 637 strict_creation_check=strict_creation_check)
639 638
640 639 if api_key:
641 640 log.info('setting a new default auth token for user `%s`', username)
642 641 UserModel().add_auth_token(
643 642 user=user, lifetime_minutes=-1,
644 643 role=UserModel.auth_token_role.ROLE_ALL,
645 644 description='BUILTIN TOKEN')
646 645
647 646 def create_default_user(self):
648 647 log.info('creating default user')
649 648 # create default user for handling default permissions.
650 649 user = UserModel().create_or_update(username=User.DEFAULT_USER,
651 650 password=str(uuid.uuid1())[:20],
652 651 email=User.DEFAULT_USER_EMAIL,
653 652 firstname='Anonymous',
654 653 lastname='User',
655 654 strict_creation_check=False)
656 655 # based on configuration options activate/de-activate this user which
657 656 # controls anonymous access
658 657 if self.cli_args.get('public_access') is False:
659 658 log.info('Public access disabled')
660 659 user.active = False
661 660 Session().add(user)
662 661 Session().commit()
663 662
664 663 def create_permissions(self):
665 664 """
666 665 Creates all permissions defined in the system
667 666 """
668 667 # module.(access|create|change|delete)_[name]
669 668 # module.(none|read|write|admin)
670 669 log.info('creating permissions')
671 670 PermissionModel(self.sa).create_permissions()
672 671
673 672 def populate_default_permissions(self):
674 673 """
675 674 Populate default permissions. It will create only the default
676 675 permissions that are missing, and not alter already defined ones
677 676 """
678 677 log.info('creating default user permissions')
679 678 PermissionModel(self.sa).create_default_user_permissions(user=User.DEFAULT_USER)
@@ -1,50 +1,53 b''
1 1
2 2
3 3 import logging
4 4 from sqlalchemy import *
5 5 from sqlalchemy.engine import reflection
6 6
7 7 from alembic.migration import MigrationContext
8 8 from alembic.operations import Operations
9 9
10 10 from rhodecode.lib.dbmigrate.versions import _reset_base
11 11 from rhodecode.model import meta, init_model_encryption
12 12
13 13
14 14 log = logging.getLogger(__name__)
15 15
16 16
17 17 def _get_indexes_list(migrate_engine, table_name):
18 18 inspector = reflection.Inspector.from_engine(migrate_engine)
19 19 return inspector.get_indexes(table_name)
20 20
21 21
22 22 def upgrade(migrate_engine):
23 23 """
24 24 Upgrade operations go here.
25 25 Don't create your own engine; bind migrate_engine to your metadata
26 26 """
27 27 from rhodecode.model import db as db_5_1_0_0
28 28
29 29 # issue fixups
30 30 fixups(db_5_1_0_0, meta.Session)
31 31
32 32
33 33 def downgrade(migrate_engine):
34 34 pass
35 35
36 36
37 37 def fixups(models, _SESSION):
38
38 39 for db_repo in _SESSION.query(models.Repository).all():
39 40
40 config = db_repo._config
41 config.set('extensions', 'largefiles', '')
41 try:
42 config = db_repo._config
43 config.set('extensions', 'largefiles', '')
42 44
43 try:
44 scm = db_repo.scm_instance(cache=False, config=config)
45 scm = db_repo.scm_instance(cache=False, config=config, vcs_full_cache=False)
45 46 if scm:
46 47 print(f'installing hook for repo: {db_repo}')
47 48 scm.install_hooks(force=True)
49 del scm # force GC
50 del config
48 51 except Exception as e:
49 52 print(e)
50 53 print('continue...')
@@ -1,205 +1,214 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 Set of custom exceptions used in RhodeCode
21 21 """
22 22
23 23 from webob.exc import HTTPClientError
24 24 from pyramid.httpexceptions import HTTPBadGateway
25 25
26 26
27 27 class LdapUsernameError(Exception):
28 28 pass
29 29
30 30
31 31 class LdapPasswordError(Exception):
32 32 pass
33 33
34 34
35 35 class LdapConnectionError(Exception):
36 36 pass
37 37
38 38
39 39 class LdapImportError(Exception):
40 40 pass
41 41
42 42
43 43 class DefaultUserException(Exception):
44 44 pass
45 45
46 46
47 47 class UserOwnsReposException(Exception):
48 48 pass
49 49
50 50
51 51 class UserOwnsRepoGroupsException(Exception):
52 52 pass
53 53
54 54
55 55 class UserOwnsUserGroupsException(Exception):
56 56 pass
57 57
58 58
59 59 class UserOwnsPullRequestsException(Exception):
60 60 pass
61 61
62 62
63 63 class UserOwnsArtifactsException(Exception):
64 64 pass
65 65
66 66
67 67 class UserGroupAssignedException(Exception):
68 68 pass
69 69
70 70
71 71 class StatusChangeOnClosedPullRequestError(Exception):
72 72 pass
73 73
74 74
75 75 class AttachedForksError(Exception):
76 76 pass
77 77
78 78
79 79 class AttachedPullRequestsError(Exception):
80 80 pass
81 81
82 82
83 class AttachedArtifactsError(Exception):
84 pass
85
86
83 87 class RepoGroupAssignmentError(Exception):
84 88 pass
85 89
86 90
87 91 class NonRelativePathError(Exception):
88 92 pass
89 93
90 94
91 95 class HTTPRequirementError(HTTPClientError):
92 96 title = explanation = 'Repository Requirement Missing'
93 97 reason = None
94 98
95 99 def __init__(self, message, *args, **kwargs):
96 100 self.title = self.explanation = message
97 101 super().__init__(*args, **kwargs)
98 102 self.args = (message, )
99 103
100 104
105 class ClientNotSupportedError(HTTPRequirementError):
106 title = explanation = 'Client Not Supported'
107 reason = None
108
109
101 110 class HTTPLockedRC(HTTPClientError):
102 111 """
103 112 Special Exception For locked Repos in RhodeCode, the return code can
104 113 be overwritten by _code keyword argument passed into constructors
105 114 """
106 115 code = 423
107 116 title = explanation = 'Repository Locked'
108 117 reason = None
109 118
110 119 def __init__(self, message, *args, **kwargs):
111 120 import rhodecode
112 121
113 122 self.code = rhodecode.ConfigGet().get_int('lock_ret_code', missing=self.code)
114 123
115 124 self.title = self.explanation = message
116 125 super().__init__(*args, **kwargs)
117 126 self.args = (message, )
118 127
119 128
120 129 class HTTPBranchProtected(HTTPClientError):
121 130 """
122 131 Special Exception For Indicating that branch is protected in RhodeCode, the
123 132 return code can be overwritten by _code keyword argument passed into constructors
124 133 """
125 134 code = 403
126 135 title = explanation = 'Branch Protected'
127 136 reason = None
128 137
129 138 def __init__(self, message, *args, **kwargs):
130 139 self.title = self.explanation = message
131 140 super().__init__(*args, **kwargs)
132 141 self.args = (message, )
133 142
134 143
135 144 class IMCCommitError(Exception):
136 145 pass
137 146
138 147
139 148 class UserCreationError(Exception):
140 149 pass
141 150
142 151
143 152 class NotAllowedToCreateUserError(Exception):
144 153 pass
145 154
146 155
147 156 class DuplicateUpdateUserError(Exception):
148 157 pass
149 158
150 159
151 160 class RepositoryCreationError(Exception):
152 161 pass
153 162
154 163
155 164 class VCSServerUnavailable(HTTPBadGateway):
156 165 """ HTTP Exception class for VCS Server errors """
157 166 code = 502
158 167 title = 'VCS Server Error'
159 168 causes = [
160 169 'VCS Server is not running',
161 170 'Incorrect vcs.server=host:port',
162 171 'Incorrect vcs.server.protocol',
163 172 ]
164 173
165 174 def __init__(self, message=''):
166 175 self.explanation = 'Could not connect to VCS Server'
167 176 if message:
168 177 self.explanation += ': ' + message
169 178 super().__init__()
170 179
171 180
172 181 class ArtifactMetadataDuplicate(ValueError):
173 182
174 183 def __init__(self, *args, **kwargs):
175 184 self.err_section = kwargs.pop('err_section', None)
176 185 self.err_key = kwargs.pop('err_key', None)
177 186 super().__init__(*args, **kwargs)
178 187
179 188
180 189 class ArtifactMetadataBadValueType(ValueError):
181 190 pass
182 191
183 192
184 193 class CommentVersionMismatch(ValueError):
185 194 pass
186 195
187 196
188 197 class SignatureVerificationError(ValueError):
189 198 pass
190 199
191 200
192 201 def signature_verification_error(msg):
193 202 details = """
194 203 Encryption signature verification failed.
195 204 Please check your value of secret key, and/or encrypted value stored.
196 205 Secret key stored inside .ini file:
197 206 `rhodecode.encrypted_values.secret` or defaults to
198 207 `beaker.session.secret`
199 208
200 209 Probably the stored values were encrypted using a different secret then currently set in .ini file
201 210 """
202 211
203 212 final_msg = f'{msg}\n{details}'
204 213 return SignatureVerificationError(final_msg)
205 214
@@ -1,2230 +1,2182 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 Helper functions
21 21
22 22 Consists of functions to typically be used within templates, but also
23 23 available to Controllers. This module is available to both as 'h'.
24 24 """
25 25 import base64
26 26 import collections
27 27
28 28 import os
29 29 import random
30 30 import hashlib
31 31 import io
32 32 import textwrap
33 33 import urllib.request
34 34 import urllib.parse
35 35 import urllib.error
36 36 import math
37 37 import logging
38 38 import re
39 39 import time
40 40 import string
41 41 import regex
42 42 from collections import OrderedDict
43 43
44 44 import pygments
45 45 import itertools
46 46 import fnmatch
47 47
48 48 from datetime import datetime
49 49 from functools import partial
50 50 from pygments.formatters.html import HtmlFormatter
51 51 from pygments.lexers import (
52 52 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
53 53
54 54 from pyramid.threadlocal import get_current_request
55 55 from tempita import looper
56 56 from webhelpers2.html import literal, HTML, escape
57 57 from webhelpers2.html._autolink import _auto_link_urls
58 58 from webhelpers2.html.tools import (
59 59 button_to, highlight, js_obfuscate, strip_links, strip_tags)
60 60
61 61 from webhelpers2.text import (
62 62 chop_at, collapse, convert_accented_entities,
63 63 convert_misc_entities, lchop, plural, rchop, remove_formatting,
64 64 replace_whitespace, urlify, truncate, wrap_paragraphs)
65 65 from webhelpers2.date import time_ago_in_words
66 66
67 67 from webhelpers2.html.tags import (
68 68 _input, NotGiven, _make_safe_id_component as safeid,
69 69 form as insecure_form,
70 70 auto_discovery_link, checkbox, end_form, file,
71 71 hidden, image, javascript_link, link_to, link_to_if, link_to_unless, ol,
72 72 stylesheet_link, submit, text, password, textarea,
73 73 ul, radio, Options)
74 74
75 75 from webhelpers2.number import format_byte_size
76 76 # python3.11 backport fixes for webhelpers2
77 77 from rhodecode import ConfigGet
78 78 from rhodecode.lib._vendor.webhelpers_backports import raw_select
79 79
80 80 from rhodecode.lib.action_parser import action_parser
81 81 from rhodecode.lib.html_filters import sanitize_html
82 82 from rhodecode.lib.pagination import Page, RepoPage, SqlPage
83 83 from rhodecode.lib import ext_json
84 from rhodecode.lib.ext_json import json
84 from rhodecode.lib.ext_json import json, formatted_str_json
85 85 from rhodecode.lib.str_utils import safe_bytes, convert_special_chars, base64_to_str
86 86 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
87 87 from rhodecode.lib.str_utils import safe_str
88 88 from rhodecode.lib.utils2 import (
89 89 str2bool,
90 90 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
91 91 AttributeDict, safe_int, md5, md5_safe, get_host_info)
92 92 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
93 93 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
94 94 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
95 95 from rhodecode.lib.vcs.conf.settings import ARCHIVE_SPECS
96 96 from rhodecode.lib.index.search_utils import get_matching_line_offsets
97 97 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
98 98 from rhodecode.model.changeset_status import ChangesetStatusModel
99 99 from rhodecode.model.db import Permission, User, Repository, UserApiKeys, FileStore
100 100 from rhodecode.model.repo_group import RepoGroupModel
101 101 from rhodecode.model.settings import IssueTrackerSettingsModel
102 102
103 103
104 104 log = logging.getLogger(__name__)
105 105
106 106
107 107 DEFAULT_USER = User.DEFAULT_USER
108 108 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
109 109
110 110
111 111 def asset(path, ver=None, **kwargs):
112 112 """
113 113 Helper to generate a static asset file path for rhodecode assets
114 114
115 115 eg. h.asset('images/image.png', ver='3923')
116 116
117 117 :param path: path of asset
118 118 :param ver: optional version query param to append as ?ver=
119 119 """
120 120 request = get_current_request()
121 121 query = {}
122 122 query.update(kwargs)
123 123 if ver:
124 124 query = {'ver': ver}
125 125 return request.static_path(
126 126 f'rhodecode:public/{path}', _query=query)
127 127
128 128
129 129 default_html_escape_table = {
130 130 ord('&'): '&amp;',
131 131 ord('<'): '&lt;',
132 132 ord('>'): '&gt;',
133 133 ord('"'): '&quot;',
134 134 ord("'"): '&#39;',
135 135 }
136 136
137 137
138 138 def html_escape(text, html_escape_table=default_html_escape_table):
139 139 """Produce entities within text."""
140 140 return text.translate(html_escape_table)
141 141
142 142
143 143 def str_json(*args, **kwargs):
144 144 return ext_json.str_json(*args, **kwargs)
145 145
146 146
147 147 def formatted_str_json(*args, **kwargs):
148 148 return ext_json.formatted_str_json(*args, **kwargs)
149 149
150 150
151 151 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
152 152 """
153 153 Truncate string ``s`` at the first occurrence of ``sub``.
154 154
155 155 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
156 156 """
157 157 suffix_if_chopped = suffix_if_chopped or ''
158 158 pos = s.find(sub)
159 159 if pos == -1:
160 160 return s
161 161
162 162 if inclusive:
163 163 pos += len(sub)
164 164
165 165 chopped = s[:pos]
166 166 left = s[pos:].strip()
167 167
168 168 if left and suffix_if_chopped:
169 169 chopped += suffix_if_chopped
170 170
171 171 return chopped
172 172
173 173
174 174 def shorter(text, size=20, prefix=False):
175 175 postfix = '...'
176 176 if len(text) > size:
177 177 if prefix:
178 178 # shorten in front
179 179 return postfix + text[-(size - len(postfix)):]
180 180 else:
181 181 return text[:size - len(postfix)] + postfix
182 182 return text
183 183
184 184
185 185 def reset(name, value=None, id=NotGiven, type="reset", **attrs):
186 186 """
187 187 Reset button
188 188 """
189 189 return _input(type, name, value, id, attrs)
190 190
191 191
192 192 def select(name, selected_values, options, id=NotGiven, **attrs):
193 193
194 194 if isinstance(options, (list, tuple)):
195 195 options_iter = options
196 196 # Handle old value,label lists ... where value also can be value,label lists
197 197 options = Options()
198 198 for opt in options_iter:
199 199 if isinstance(opt, tuple) and len(opt) == 2:
200 200 value, label = opt
201 201 elif isinstance(opt, str):
202 202 value = label = opt
203 203 else:
204 204 raise ValueError('invalid select option type %r' % type(opt))
205 205
206 206 if isinstance(value, (list, tuple)):
207 207 option_group = options.add_optgroup(label)
208 208 for opt2 in value:
209 209 if isinstance(opt2, tuple) and len(opt2) == 2:
210 210 group_value, group_label = opt2
211 211 elif isinstance(opt2, str):
212 212 group_value = group_label = opt2
213 213 else:
214 214 raise ValueError('invalid select option type %r' % type(opt2))
215 215
216 216 option_group.add_option(group_label, group_value)
217 217 else:
218 218 options.add_option(label, value)
219 219
220 220 return raw_select(name, selected_values, options, id=id, **attrs)
221 221
222 222
223 223 def branding(name, length=40):
224 224 return truncate(name, length, indicator="")
225 225
226 226
227 227 def FID(raw_id, path):
228 228 """
229 229 Creates a unique ID for filenode based on it's hash of path and commit
230 230 it's safe to use in urls
231 231
232 232 :param raw_id:
233 233 :param path:
234 234 """
235 235
236 236 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
237 237
238 238
239 239 class _GetError(object):
240 240 """Get error from form_errors, and represent it as span wrapped error
241 241 message
242 242
243 243 :param field_name: field to fetch errors for
244 244 :param form_errors: form errors dict
245 245 """
246 246
247 247 def __call__(self, field_name, form_errors):
248 248 tmpl = """<span class="error_msg">%s</span>"""
249 249 if form_errors and field_name in form_errors:
250 250 return literal(tmpl % form_errors.get(field_name))
251 251
252 252
253 253 get_error = _GetError()
254 254
255 255
256 256 class _ToolTip(object):
257 257
258 258 def __call__(self, tooltip_title, trim_at=50):
259 259 """
260 260 Special function just to wrap our text into nice formatted
261 261 autowrapped text
262 262
263 263 :param tooltip_title:
264 264 """
265 265 tooltip_title = escape(tooltip_title)
266 266 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
267 267 return tooltip_title
268 268
269 269
270 270 tooltip = _ToolTip()
271 271
272 272 files_icon = '<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy file path"></i>'
273 273
274 274
275 275 def files_breadcrumbs(repo_name, repo_type, commit_id, file_path, landing_ref_name=None, at_ref=None,
276 276 limit_items=False, linkify_last_item=False, hide_last_item=False,
277 277 copy_path_icon=True):
278 278
279 279 if at_ref:
280 280 route_qry = {'at': at_ref}
281 281 default_landing_ref = at_ref or landing_ref_name or commit_id
282 282 else:
283 283 route_qry = None
284 284 default_landing_ref = commit_id
285 285
286 286 # first segment is a `HOME` link to repo files root location
287 287 root_name = literal('<i class="icon-home"></i>')
288 288
289 289 url_segments = [
290 290 link_to(
291 291 root_name,
292 292 repo_files_by_ref_url(
293 293 repo_name,
294 294 repo_type,
295 295 f_path=None, # None here is a special case for SVN repos,
296 296 # that won't prefix with a ref
297 297 ref_name=default_landing_ref,
298 298 commit_id=commit_id,
299 299 query=route_qry
300 300 )
301 301 )]
302 302
303 303 path_segments = file_path.split('/')
304 304 last_cnt = len(path_segments) - 1
305 305 for cnt, segment in enumerate(path_segments):
306 306 if not segment:
307 307 continue
308 308 segment_html = escape(segment)
309 309
310 310 last_item = cnt == last_cnt
311 311
312 312 if last_item and hide_last_item:
313 313 # iterate over and hide last element
314 314 continue
315 315
316 316 if last_item and linkify_last_item is False:
317 317 # plain version
318 318 url_segments.append(segment_html)
319 319 else:
320 320 url_segments.append(
321 321 link_to(
322 322 segment_html,
323 323 repo_files_by_ref_url(
324 324 repo_name,
325 325 repo_type,
326 326 f_path='/'.join(path_segments[:cnt + 1]),
327 327 ref_name=default_landing_ref,
328 328 commit_id=commit_id,
329 329 query=route_qry
330 330 ),
331 331 ))
332 332
333 333 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
334 334 if limit_items and len(limited_url_segments) < len(url_segments):
335 335 url_segments = limited_url_segments
336 336
337 337 full_path = file_path
338 338 if copy_path_icon:
339 339 icon = files_icon.format(escape(full_path))
340 340 else:
341 341 icon = ''
342 342
343 343 if file_path == '':
344 344 return root_name
345 345 else:
346 346 return literal(' / '.join(url_segments) + icon)
347 347
348 348
349 349 def files_url_data(request):
350 350 matchdict = request.matchdict
351 351
352 352 if 'f_path' not in matchdict:
353 353 matchdict['f_path'] = ''
354 354 else:
355 355 matchdict['f_path'] = urllib.parse.quote(safe_str(matchdict['f_path']))
356 356 if 'commit_id' not in matchdict:
357 357 matchdict['commit_id'] = 'tip'
358 358
359 359 return ext_json.str_json(matchdict)
360 360
361 361
362 362 def repo_files_by_ref_url(db_repo_name, db_repo_type, f_path, ref_name, commit_id, query=None, ):
363 363 _is_svn = is_svn(db_repo_type)
364 364 final_f_path = f_path
365 365
366 366 if _is_svn:
367 367 """
368 368 For SVN the ref_name cannot be used as a commit_id, it needs to be prefixed with
369 369 actually commit_id followed by the ref_name. This should be done only in case
370 370 This is a initial landing url, without additional paths.
371 371
372 372 like: /1000/tags/1.0.0/?at=tags/1.0.0
373 373 """
374 374
375 375 if ref_name and ref_name != 'tip':
376 376 # NOTE(marcink): for svn the ref_name is actually the stored path, so we prefix it
377 377 # for SVN we only do this magic prefix if it's root, .eg landing revision
378 378 # of files link. If we are in the tree we don't need this since we traverse the url
379 379 # that has everything stored
380 380 if f_path in ['', '/']:
381 381 final_f_path = '/'.join([ref_name, f_path])
382 382
383 383 # SVN always needs a commit_id explicitly, without a named REF
384 384 default_commit_id = commit_id
385 385 else:
386 386 """
387 387 For git and mercurial we construct a new URL using the names instead of commit_id
388 388 like: /master/some_path?at=master
389 389 """
390 390 # We currently do not support branches with slashes
391 391 if '/' in ref_name:
392 392 default_commit_id = commit_id
393 393 else:
394 394 default_commit_id = ref_name
395 395
396 396 # sometimes we pass f_path as None, to indicate explicit no prefix,
397 397 # we translate it to string to not have None
398 398 final_f_path = final_f_path or ''
399 399
400 400 files_url = route_path(
401 401 'repo_files',
402 402 repo_name=db_repo_name,
403 403 commit_id=default_commit_id,
404 404 f_path=final_f_path,
405 405 _query=query
406 406 )
407 407 return files_url
408 408
409 409
410 410 def code_highlight(code, lexer, formatter, use_hl_filter=False):
411 411 """
412 412 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
413 413
414 414 If ``outfile`` is given and a valid file object (an object
415 415 with a ``write`` method), the result will be written to it, otherwise
416 416 it is returned as a string.
417 417 """
418 418 if use_hl_filter:
419 419 # add HL filter
420 420 from rhodecode.lib.index import search_utils
421 421 lexer.add_filter(search_utils.ElasticSearchHLFilter())
422 422 return pygments.format(pygments.lex(code, lexer), formatter)
423 423
424 424
425 425 class CodeHtmlFormatter(HtmlFormatter):
426 426 """
427 427 My code Html Formatter for source codes
428 428 """
429 429
430 430 def wrap(self, source):
431 431 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
432 432
433 433 def _wrap_code(self, source):
434 434 for cnt, it in enumerate(source):
435 435 i, t = it
436 436 t = f'<div id="L{cnt+1}">{t}</div>'
437 437 yield i, t
438 438
439 439 def _wrap_tablelinenos(self, inner):
440 440 dummyoutfile = io.StringIO()
441 441 lncount = 0
442 442 for t, line in inner:
443 443 if t:
444 444 lncount += 1
445 445 dummyoutfile.write(line)
446 446
447 447 fl = self.linenostart
448 448 mw = len(str(lncount + fl - 1))
449 449 sp = self.linenospecial
450 450 st = self.linenostep
451 451 la = self.lineanchors
452 452 aln = self.anchorlinenos
453 453 nocls = self.noclasses
454 454 if sp:
455 455 lines = []
456 456
457 457 for i in range(fl, fl + lncount):
458 458 if i % st == 0:
459 459 if i % sp == 0:
460 460 if aln:
461 461 lines.append('<a href="#%s%d" class="special">%*d</a>' %
462 462 (la, i, mw, i))
463 463 else:
464 464 lines.append('<span class="special">%*d</span>' % (mw, i))
465 465 else:
466 466 if aln:
467 467 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
468 468 else:
469 469 lines.append('%*d' % (mw, i))
470 470 else:
471 471 lines.append('')
472 472 ls = '\n'.join(lines)
473 473 else:
474 474 lines = []
475 475 for i in range(fl, fl + lncount):
476 476 if i % st == 0:
477 477 if aln:
478 478 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
479 479 else:
480 480 lines.append('%*d' % (mw, i))
481 481 else:
482 482 lines.append('')
483 483 ls = '\n'.join(lines)
484 484
485 485 # in case you wonder about the seemingly redundant <div> here: since the
486 486 # content in the other cell also is wrapped in a div, some browsers in
487 487 # some configurations seem to mess up the formatting...
488 488 if nocls:
489 489 yield 0, ('<table class="%stable">' % self.cssclass +
490 490 '<tr><td><div class="linenodiv" '
491 491 'style="background-color: #f0f0f0; padding-right: 10px">'
492 492 '<pre style="line-height: 125%">' +
493 493 ls + '</pre></div></td><td id="hlcode" class="code">')
494 494 else:
495 495 yield 0, ('<table class="%stable">' % self.cssclass +
496 496 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
497 497 ls + '</pre></div></td><td id="hlcode" class="code">')
498 498 yield 0, dummyoutfile.getvalue()
499 499 yield 0, '</td></tr></table>'
500 500
501 501
502 502 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
503 503 def __init__(self, **kw):
504 504 # only show these line numbers if set
505 505 self.only_lines = kw.pop('only_line_numbers', [])
506 506 self.query_terms = kw.pop('query_terms', [])
507 507 self.max_lines = kw.pop('max_lines', 5)
508 508 self.line_context = kw.pop('line_context', 3)
509 509 self.url = kw.pop('url', None)
510 510
511 511 super(CodeHtmlFormatter, self).__init__(**kw)
512 512
513 513 def _wrap_code(self, source):
514 514 for cnt, it in enumerate(source):
515 515 i, t = it
516 516 t = '<pre>%s</pre>' % t
517 517 yield i, t
518 518
519 519 def _wrap_tablelinenos(self, inner):
520 520 yield 0, '<table class="code-highlight %stable">' % self.cssclass
521 521
522 522 last_shown_line_number = 0
523 523 current_line_number = 1
524 524
525 525 for t, line in inner:
526 526 if not t:
527 527 yield t, line
528 528 continue
529 529
530 530 if current_line_number in self.only_lines:
531 531 if last_shown_line_number + 1 != current_line_number:
532 532 yield 0, '<tr>'
533 533 yield 0, '<td class="line">...</td>'
534 534 yield 0, '<td id="hlcode" class="code"></td>'
535 535 yield 0, '</tr>'
536 536
537 537 yield 0, '<tr>'
538 538 if self.url:
539 539 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
540 540 self.url, current_line_number, current_line_number)
541 541 else:
542 542 yield 0, '<td class="line"><a href="">%i</a></td>' % (
543 543 current_line_number)
544 544 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
545 545 yield 0, '</tr>'
546 546
547 547 last_shown_line_number = current_line_number
548 548
549 549 current_line_number += 1
550 550
551 551 yield 0, '</table>'
552 552
553 553
554 554 def hsv_to_rgb(h, s, v):
555 555 """ Convert hsv color values to rgb """
556 556
557 557 if s == 0.0:
558 558 return v, v, v
559 559 i = int(h * 6.0) # XXX assume int() truncates!
560 560 f = (h * 6.0) - i
561 561 p = v * (1.0 - s)
562 562 q = v * (1.0 - s * f)
563 563 t = v * (1.0 - s * (1.0 - f))
564 564 i = i % 6
565 565 if i == 0:
566 566 return v, t, p
567 567 if i == 1:
568 568 return q, v, p
569 569 if i == 2:
570 570 return p, v, t
571 571 if i == 3:
572 572 return p, q, v
573 573 if i == 4:
574 574 return t, p, v
575 575 if i == 5:
576 576 return v, p, q
577 577
578 578
579 579 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
580 580 """
581 581 Generator for getting n of evenly distributed colors using
582 582 hsv color and golden ratio. It always return same order of colors
583 583
584 584 :param n: number of colors to generate
585 585 :param saturation: saturation of returned colors
586 586 :param lightness: lightness of returned colors
587 587 :returns: RGB tuple
588 588 """
589 589
590 590 golden_ratio = 0.618033988749895
591 591 h = 0.22717784590367374
592 592
593 593 for _ in range(n):
594 594 h += golden_ratio
595 595 h %= 1
596 596 HSV_tuple = [h, saturation, lightness]
597 597 RGB_tuple = hsv_to_rgb(*HSV_tuple)
598 598 yield [str(int(x * 256)) for x in RGB_tuple]
599 599
600 600
601 601 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
602 602 """
603 603 Returns a function which when called with an argument returns a unique
604 604 color for that argument, eg.
605 605
606 606 :param n: number of colors to generate
607 607 :param saturation: saturation of returned colors
608 608 :param lightness: lightness of returned colors
609 609 :returns: css RGB string
610 610
611 611 >>> color_hash = color_hasher()
612 612 >>> color_hash('hello')
613 613 'rgb(34, 12, 59)'
614 614 >>> color_hash('hello')
615 615 'rgb(34, 12, 59)'
616 616 >>> color_hash('other')
617 617 'rgb(90, 224, 159)'
618 618 """
619 619
620 620 color_dict = {}
621 621 cgenerator = unique_color_generator(
622 622 saturation=saturation, lightness=lightness)
623 623
624 624 def get_color_string(thing):
625 625 if thing in color_dict:
626 626 col = color_dict[thing]
627 627 else:
628 628 col = color_dict[thing] = next(cgenerator)
629 629 return "rgb(%s)" % (', '.join(col))
630 630
631 631 return get_color_string
632 632
633 633
634 634 def get_lexer_safe(mimetype=None, filepath=None):
635 635 """
636 636 Tries to return a relevant pygments lexer using mimetype/filepath name,
637 637 defaulting to plain text if none could be found
638 638 """
639 639 lexer = None
640 640 try:
641 641 if mimetype:
642 642 lexer = get_lexer_for_mimetype(mimetype)
643 643 if not lexer:
644 644 lexer = get_lexer_for_filename(filepath)
645 645 except pygments.util.ClassNotFound:
646 646 pass
647 647
648 648 if not lexer:
649 649 lexer = get_lexer_by_name('text')
650 650
651 651 return lexer
652 652
653 653
654 654 def get_lexer_for_filenode(filenode):
655 655 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
656 656 return lexer
657 657
658 658
659 659 def pygmentize(filenode, **kwargs):
660 660 """
661 661 pygmentize function using pygments
662 662
663 663 :param filenode:
664 664 """
665 665 lexer = get_lexer_for_filenode(filenode)
666 666 return literal(code_highlight(filenode.content, lexer,
667 667 CodeHtmlFormatter(**kwargs)))
668 668
669 669
670 670 def is_following_repo(repo_name, user_id):
671 671 from rhodecode.model.scm import ScmModel
672 672 return ScmModel().is_following_repo(repo_name, user_id)
673 673
674 674
675 675 class _Message(object):
676 676 """A message returned by ``Flash.pop_messages()``.
677 677
678 678 Converting the message to a string returns the message text. Instances
679 679 also have the following attributes:
680 680
681 681 * ``message``: the message text.
682 682 * ``category``: the category specified when the message was created.
683 683 """
684 684
685 685 def __init__(self, category, message, sub_data=None):
686 686 self.category = category
687 687 self.message = message
688 688 self.sub_data = sub_data or {}
689 689
690 690 def __str__(self):
691 691 return self.message
692 692
693 693 __unicode__ = __str__
694 694
695 695 def __html__(self):
696 696 return escape(safe_str(self.message))
697 697
698 698
699 699 class Flash(object):
700 700 # List of allowed categories. If None, allow any category.
701 701 categories = ["warning", "notice", "error", "success"]
702 702
703 703 # Default category if none is specified.
704 704 default_category = "notice"
705 705
706 706 def __init__(self, session_key="flash", categories=None,
707 707 default_category=None):
708 708 """
709 709 Instantiate a ``Flash`` object.
710 710
711 711 ``session_key`` is the key to save the messages under in the user's
712 712 session.
713 713
714 714 ``categories`` is an optional list which overrides the default list
715 715 of categories.
716 716
717 717 ``default_category`` overrides the default category used for messages
718 718 when none is specified.
719 719 """
720 720 self.session_key = session_key
721 721 if categories is not None:
722 722 self.categories = categories
723 723 if default_category is not None:
724 724 self.default_category = default_category
725 725 if self.categories and self.default_category not in self.categories:
726 726 raise ValueError(
727 727 "unrecognized default category %r" % (self.default_category,))
728 728
729 729 def pop_messages(self, session=None, request=None):
730 730 """
731 731 Return all accumulated messages and delete them from the session.
732 732
733 733 The return value is a list of ``Message`` objects.
734 734 """
735 735 messages = []
736 736
737 737 if not session:
738 738 if not request:
739 739 request = get_current_request()
740 740 session = request.session
741 741
742 742 # Pop the 'old' pylons flash messages. They are tuples of the form
743 743 # (category, message)
744 744 for cat, msg in session.pop(self.session_key, []):
745 745 messages.append(_Message(cat, msg))
746 746
747 747 # Pop the 'new' pyramid flash messages for each category as list
748 748 # of strings.
749 749 for cat in self.categories:
750 750 for msg in session.pop_flash(queue=cat):
751 751 sub_data = {}
752 752 if hasattr(msg, 'rsplit'):
753 753 flash_data = msg.rsplit('|DELIM|', 1)
754 754 org_message = flash_data[0]
755 755 if len(flash_data) > 1:
756 756 sub_data = json.loads(flash_data[1])
757 757 else:
758 758 org_message = msg
759 759
760 760 messages.append(_Message(cat, org_message, sub_data=sub_data))
761 761
762 762 # Map messages from the default queue to the 'notice' category.
763 763 for msg in session.pop_flash():
764 764 messages.append(_Message('notice', msg))
765 765
766 766 session.save()
767 767 return messages
768 768
769 769 def json_alerts(self, session=None, request=None):
770 770 payloads = []
771 771 messages = flash.pop_messages(session=session, request=request) or []
772 772 for message in messages:
773 773 payloads.append({
774 774 'message': {
775 775 'message': '{}'.format(message.message),
776 776 'level': message.category,
777 777 'force': True,
778 778 'subdata': message.sub_data
779 779 }
780 780 })
781 781 return safe_str(json.dumps(payloads))
782 782
783 783 def __call__(self, message, category=None, ignore_duplicate=True,
784 784 session=None, request=None):
785 785
786 786 if not session:
787 787 if not request:
788 788 request = get_current_request()
789 789 session = request.session
790 790
791 791 session.flash(
792 792 message, queue=category, allow_duplicate=not ignore_duplicate)
793 793
794 794
795 795 flash = Flash()
796 796
797 797 #==============================================================================
798 798 # SCM FILTERS available via h.
799 799 #==============================================================================
800 800 from rhodecode.lib.vcs.utils import author_name, author_email
801 801 from rhodecode.lib.utils2 import age, age_from_seconds
802 802 from rhodecode.model.db import User, ChangesetStatus
803 803
804 804
805 805 email = author_email
806 806
807 807
808 808 def capitalize(raw_text):
809 809 return raw_text.capitalize()
810 810
811 811
812 812 def short_id(long_id):
813 813 return long_id[:12]
814 814
815 815
816 816 def hide_credentials(url):
817 817 from rhodecode.lib.utils2 import credentials_filter
818 818 return credentials_filter(url)
819 819
820 820 import zoneinfo
821 821 import tzlocal
822 822 local_timezone = tzlocal.get_localzone()
823 823
824 824
825 825 def get_timezone(datetime_iso, time_is_local=False):
826 826 tzinfo = '+00:00'
827 827
828 828 # detect if we have a timezone info, otherwise, add it
829 829 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
830 830 force_timezone = os.environ.get('RC_TIMEZONE', '')
831 831 if force_timezone:
832 832 force_timezone = zoneinfo.ZoneInfo(force_timezone)
833 833 timezone = force_timezone or local_timezone
834 834
835 835 offset = datetime_iso.replace(tzinfo=timezone).strftime('%z')
836 836 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
837 837 return tzinfo
838 838
839 839
840 840 def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
841 841 title = value or format_date(datetime_iso)
842 842 tzinfo = get_timezone(datetime_iso, time_is_local=time_is_local)
843 843
844 844 return literal(
845 845 '<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
846 846 cls='tooltip' if tooltip else '',
847 847 tt_title=('{title}{tzinfo}'.format(title=title, tzinfo=tzinfo)) if tooltip else '',
848 848 title=title, dt=datetime_iso, tzinfo=tzinfo
849 849 ))
850 850
851 851
852 852 def _shorten_commit_id(commit_id, commit_len=None):
853 853 if commit_len is None:
854 854 request = get_current_request()
855 855 commit_len = request.call_context.visual.show_sha_length
856 856 return commit_id[:commit_len]
857 857
858 858
859 859 def show_id(commit, show_idx=None, commit_len=None):
860 860 """
861 861 Configurable function that shows ID
862 862 by default it's r123:fffeeefffeee
863 863
864 864 :param commit: commit instance
865 865 """
866 866 if show_idx is None:
867 867 request = get_current_request()
868 868 show_idx = request.call_context.visual.show_revision_number
869 869
870 870 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
871 871 if show_idx:
872 872 return 'r%s:%s' % (commit.idx, raw_id)
873 873 else:
874 874 return '%s' % (raw_id, )
875 875
876 876
877 877 def format_date(date):
878 878 """
879 879 use a standardized formatting for dates used in RhodeCode
880 880
881 881 :param date: date/datetime object
882 882 :return: formatted date
883 883 """
884 884
885 885 if date:
886 886 _fmt = "%a, %d %b %Y %H:%M:%S"
887 887 return safe_str(date.strftime(_fmt))
888 888
889 889 return ""
890 890
891 891
892 892 class _RepoChecker(object):
893 893
894 894 def __init__(self, backend_alias):
895 895 self._backend_alias = backend_alias
896 896
897 897 def __call__(self, repository):
898 898 if hasattr(repository, 'alias'):
899 899 _type = repository.alias
900 900 elif hasattr(repository, 'repo_type'):
901 901 _type = repository.repo_type
902 902 else:
903 903 _type = repository
904 904 return _type == self._backend_alias
905 905
906 906
907 907 is_git = _RepoChecker('git')
908 908 is_hg = _RepoChecker('hg')
909 909 is_svn = _RepoChecker('svn')
910 910
911 911
912 912 def get_repo_type_by_name(repo_name):
913 913 repo = Repository.get_by_repo_name(repo_name)
914 914 if repo:
915 915 return repo.repo_type
916 916
917 917
918 918 def is_svn_without_proxy(repository):
919 919 if is_svn(repository):
920 920 return not ConfigGet().get_bool('vcs.svn.proxy.enabled')
921 921 return False
922 922
923 923
924 924 def discover_user(author):
925 925 """
926 926 Tries to discover RhodeCode User based on the author string. Author string
927 927 is typically `FirstName LastName <email@address.com>`
928 928 """
929 929
930 930 # if author is already an instance use it for extraction
931 931 if isinstance(author, User):
932 932 return author
933 933
934 934 # Valid email in the attribute passed, see if they're in the system
935 935 _email = author_email(author)
936 936 if _email != '':
937 937 user = User.get_by_email(_email, case_insensitive=True, cache=True)
938 938 if user is not None:
939 939 return user
940 940
941 941 # Maybe it's a username, we try to extract it and fetch by username ?
942 942 _author = author_name(author)
943 943 user = User.get_by_username(_author, case_insensitive=True, cache=True)
944 944 if user is not None:
945 945 return user
946 946
947 947 return None
948 948
949 949
950 950 def email_or_none(author):
951 951 # extract email from the commit string
952 952 _email = author_email(author)
953 953
954 954 # If we have an email, use it, otherwise
955 955 # see if it contains a username we can get an email from
956 956 if _email != '':
957 957 return _email
958 958 else:
959 959 user = User.get_by_username(
960 960 author_name(author), case_insensitive=True, cache=True)
961 961
962 962 if user is not None:
963 963 return user.email
964 964
965 965 # No valid email, not a valid user in the system, none!
966 966 return None
967 967
968 968
969 969 def link_to_user(author, length=0, **kwargs):
970 970 user = discover_user(author)
971 971 # user can be None, but if we have it already it means we can re-use it
972 972 # in the person() function, so we save 1 intensive-query
973 973 if user:
974 974 author = user
975 975
976 976 display_person = person(author, 'username_or_name_or_email')
977 977 if length:
978 978 display_person = shorter(display_person, length)
979 979
980 980 if user and user.username != user.DEFAULT_USER:
981 981 return link_to(
982 982 escape(display_person),
983 983 route_path('user_profile', username=user.username),
984 984 **kwargs)
985 985 else:
986 986 return escape(display_person)
987 987
988 988
989 989 def link_to_group(users_group_name, **kwargs):
990 990 return link_to(
991 991 escape(users_group_name),
992 992 route_path('user_group_profile', user_group_name=users_group_name),
993 993 **kwargs)
994 994
995 995
996 996 def person(author, show_attr="username_and_name"):
997 997 user = discover_user(author)
998 998 if user:
999 999 return getattr(user, show_attr)
1000 1000 else:
1001 1001 _author = author_name(author)
1002 1002 _email = email(author)
1003 1003 return _author or _email
1004 1004
1005 1005
1006 1006 def author_string(email):
1007 1007 if email:
1008 1008 user = User.get_by_email(email, case_insensitive=True, cache=True)
1009 1009 if user:
1010 1010 if user.first_name or user.last_name:
1011 1011 return '%s %s &lt;%s&gt;' % (
1012 1012 user.first_name, user.last_name, email)
1013 1013 else:
1014 1014 return email
1015 1015 else:
1016 1016 return email
1017 1017 else:
1018 1018 return None
1019 1019
1020 1020
1021 1021 def person_by_id(id_, show_attr="username_and_name"):
1022 1022 # attr to return from fetched user
1023 1023 def person_getter(usr):
1024 1024 return getattr(usr, show_attr)
1025 1025
1026 1026 #maybe it's an ID ?
1027 1027 if str(id_).isdigit() or isinstance(id_, int):
1028 1028 id_ = int(id_)
1029 1029 user = User.get(id_)
1030 1030 if user is not None:
1031 1031 return person_getter(user)
1032 1032 return id_
1033 1033
1034 1034
1035 1035 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
1036 1036 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
1037 1037 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
1038 1038
1039 1039
1040 1040 tags_patterns = OrderedDict(
1041 1041 (
1042 1042 (
1043 1043 "lang",
1044 1044 (
1045 1045 re.compile(r"\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]"),
1046 1046 '<div class="metatag" tag="lang">\\2</div>',
1047 1047 ),
1048 1048 ),
1049 1049 (
1050 1050 "see",
1051 1051 (
1052 1052 re.compile(r"\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]"),
1053 1053 '<div class="metatag" tag="see">see: \\1 </div>',
1054 1054 ),
1055 1055 ),
1056 1056 (
1057 1057 "url",
1058 1058 (
1059 1059 re.compile(
1060 1060 r"\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]"
1061 1061 ),
1062 1062 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>',
1063 1063 ),
1064 1064 ),
1065 1065 (
1066 1066 "license",
1067 1067 (
1068 1068 re.compile(
1069 1069 r"\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]"
1070 1070 ),
1071 1071 # don't make it a raw string here...
1072 1072 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>',
1073 1073 ),
1074 1074 ),
1075 1075 (
1076 1076 "ref",
1077 1077 (
1078 1078 re.compile(
1079 1079 r"\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]"
1080 1080 ),
1081 1081 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>',
1082 1082 ),
1083 1083 ),
1084 1084 (
1085 1085 "state",
1086 1086 (
1087 1087 re.compile(r"\[(stable|featured|stale|dead|dev|deprecated)\]"),
1088 1088 '<div class="metatag" tag="state \\1">\\1</div>',
1089 1089 ),
1090 1090 ),
1091 1091 # label in grey
1092 1092 (
1093 1093 "label",
1094 1094 (re.compile(r"\[([a-z]+)\]"), '<div class="metatag" tag="label">\\1</div>'),
1095 1095 ),
1096 1096 # generic catch all in grey
1097 1097 (
1098 1098 "generic",
1099 1099 (
1100 1100 re.compile(r"\[([a-zA-Z0-9\.\-\_]+)\]"),
1101 1101 '<div class="metatag" tag="generic">\\1</div>',
1102 1102 ),
1103 1103 ),
1104 1104 )
1105 1105 )
1106 1106
1107 1107
1108 1108 def extract_metatags(value):
1109 1109 """
1110 1110 Extract supported meta-tags from given text value
1111 1111 """
1112 1112 tags = []
1113 1113 if not value:
1114 1114 return tags, ''
1115 1115
1116 1116 for key, val in list(tags_patterns.items()):
1117 1117 pat, replace_html = val
1118 1118 tags.extend([(key, x.group()) for x in pat.finditer(value)])
1119 1119 value = pat.sub('', value)
1120 1120
1121 1121 return tags, value
1122 1122
1123 1123
1124 1124 def style_metatag(tag_type, value):
1125 1125 """
1126 1126 converts tags from value into html equivalent
1127 1127 """
1128 1128 if not value:
1129 1129 return ''
1130 1130
1131 1131 html_value = value
1132 1132 tag_data = tags_patterns.get(tag_type)
1133 1133 if tag_data:
1134 1134 pat, replace_html = tag_data
1135 1135 # convert to plain `str` instead of a markup tag to be used in
1136 1136 # regex expressions. safe_str doesn't work here
1137 1137 html_value = pat.sub(replace_html, value)
1138 1138
1139 1139 return html_value
1140 1140
1141 1141
1142 1142 def bool2icon(value, show_at_false=True):
1143 1143 """
1144 1144 Returns boolean value of a given value, represented as html element with
1145 1145 classes that will represent icons
1146 1146
1147 1147 :param value: given value to convert to html node
1148 1148 """
1149 1149
1150 1150 if value: # does bool conversion
1151 1151 return HTML.tag('i', class_="icon-true", title='True')
1152 1152 else: # not true as bool
1153 1153 if show_at_false:
1154 1154 return HTML.tag('i', class_="icon-false", title='False')
1155 1155 return HTML.tag('i')
1156 1156
1157 1157
1158 1158 def b64(inp):
1159 1159 return base64.b64encode(safe_bytes(inp))
1160 1160
1161 1161 #==============================================================================
1162 1162 # PERMS
1163 1163 #==============================================================================
1164 1164 from rhodecode.lib.auth import (
1165 1165 HasPermissionAny, HasPermissionAll,
1166 1166 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll,
1167 1167 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token,
1168 1168 csrf_token_key, AuthUser)
1169 1169
1170 1170
1171 1171 #==============================================================================
1172 1172 # GRAVATAR URL
1173 1173 #==============================================================================
1174 1174 class InitialsGravatar(object):
1175 1175 def __init__(self, email_address, first_name, last_name, size=30,
1176 1176 background=None, text_color='#fff'):
1177 1177 self.size = size
1178 1178 self.first_name = first_name
1179 1179 self.last_name = last_name
1180 1180 self.email_address = email_address
1181 1181 self.background = background or self.str2color(email_address)
1182 1182 self.text_color = text_color
1183 1183
1184 1184 def get_color_bank(self):
1185 1185 """
1186 1186 returns a predefined list of colors that gravatars can use.
1187 1187 Those are randomized distinct colors that guarantee readability and
1188 1188 uniqueness.
1189 1189
1190 1190 generated with: http://phrogz.net/css/distinct-colors.html
1191 1191 """
1192 1192 return [
1193 1193 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1194 1194 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1195 1195 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1196 1196 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1197 1197 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1198 1198 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1199 1199 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1200 1200 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1201 1201 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1202 1202 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1203 1203 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1204 1204 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1205 1205 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1206 1206 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1207 1207 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1208 1208 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1209 1209 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1210 1210 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1211 1211 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1212 1212 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1213 1213 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1214 1214 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1215 1215 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1216 1216 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1217 1217 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1218 1218 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1219 1219 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1220 1220 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1221 1221 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1222 1222 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1223 1223 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1224 1224 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1225 1225 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1226 1226 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1227 1227 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1228 1228 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1229 1229 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1230 1230 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1231 1231 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1232 1232 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1233 1233 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1234 1234 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1235 1235 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1236 1236 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1237 1237 '#4f8c46', '#368dd9', '#5c0073'
1238 1238 ]
1239 1239
1240 1240 def rgb_to_hex_color(self, rgb_tuple):
1241 1241 """
1242 1242 Converts an rgb_tuple passed to an hex color.
1243 1243
1244 1244 :param rgb_tuple: tuple with 3 ints represents rgb color space
1245 1245 """
1246 1246 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1247 1247
1248 1248 def email_to_int_list(self, email_str):
1249 1249 """
1250 1250 Get every byte of the hex digest value of email and turn it to integer.
1251 1251 It's going to be always between 0-255
1252 1252 """
1253 1253 digest = md5_safe(email_str.lower())
1254 1254 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1255 1255
1256 1256 def pick_color_bank_index(self, email_str, color_bank):
1257 1257 return self.email_to_int_list(email_str)[0] % len(color_bank)
1258 1258
1259 1259 def str2color(self, email_str):
1260 1260 """
1261 1261 Tries to map in a stable algorithm an email to color
1262 1262
1263 1263 :param email_str:
1264 1264 """
1265 1265 color_bank = self.get_color_bank()
1266 1266 # pick position (module it's length so we always find it in the
1267 1267 # bank even if it's smaller than 256 values
1268 1268 pos = self.pick_color_bank_index(email_str, color_bank)
1269 1269 return color_bank[pos]
1270 1270
1271 1271 def normalize_email(self, email_address):
1272 1272 # default host used to fill in the fake/missing email
1273 1273 default_host = 'localhost'
1274 1274
1275 1275 if not email_address:
1276 1276 email_address = f'{User.DEFAULT_USER}@{default_host}'
1277 1277
1278 1278 email_address = safe_str(email_address)
1279 1279
1280 1280 if '@' not in email_address:
1281 1281 email_address = f'{email_address}@{default_host}'
1282 1282
1283 1283 if email_address.endswith('@'):
1284 1284 email_address = f'{email_address}{default_host}'
1285 1285
1286 1286 email_address = convert_special_chars(email_address)
1287 1287
1288 1288 return email_address
1289 1289
1290 1290 def get_initials(self):
1291 1291 """
1292 1292 Returns 2 letter initials calculated based on the input.
1293 1293 The algorithm picks first given email address, and takes first letter
1294 1294 of part before @, and then the first letter of server name. In case
1295 1295 the part before @ is in a format of `somestring.somestring2` it replaces
1296 1296 the server letter with first letter of somestring2
1297 1297
1298 1298 In case function was initialized with both first and lastname, this
1299 1299 overrides the extraction from email by first letter of the first and
1300 1300 last name. We add special logic to that functionality, In case Full name
1301 1301 is compound, like Guido Von Rossum, we use last part of the last name
1302 1302 (Von Rossum) picking `R`.
1303 1303
1304 1304 Function also normalizes the non-ascii characters to they ascii
1305 1305 representation, eg Ą => A
1306 1306 """
1307 1307 # replace non-ascii to ascii
1308 1308 first_name = convert_special_chars(self.first_name)
1309 1309 last_name = convert_special_chars(self.last_name)
1310 1310 # multi word last names, Guido Von Rossum, we take the last part only
1311 1311 last_name = last_name.split(' ', 1)[-1]
1312 1312
1313 1313 # do NFKD encoding, and also make sure email has proper format
1314 1314 email_address = self.normalize_email(self.email_address)
1315 1315
1316 1316 # first push the email initials
1317 1317 prefix, server = email_address.split('@', 1)
1318 1318
1319 1319 # check if prefix is maybe a 'first_name.last_name' syntax
1320 1320 _dot_split = prefix.rsplit('.', 1)
1321 1321 if len(_dot_split) == 2 and _dot_split[1]:
1322 1322 initials = [_dot_split[0][0], _dot_split[1][0]]
1323 1323 else:
1324 1324 initials = [prefix[0], server[0]]
1325 1325
1326 1326 # get first letter of first and last names to create initials
1327 1327 fn_letter = (first_name or " ")[0].strip()
1328 1328 ln_letter = (last_name or " ")[0].strip()
1329 1329
1330 1330 if fn_letter:
1331 1331 initials[0] = fn_letter
1332 1332
1333 1333 if ln_letter:
1334 1334 initials[1] = ln_letter
1335 1335
1336 1336 return ''.join(initials).upper()
1337 1337
1338 1338 def get_img_data_by_type(self, font_family, img_type):
1339 1339 default_user = """
1340 1340 <svg xmlns="http://www.w3.org/2000/svg"
1341 1341 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1342 1342 viewBox="-15 -10 439.165 429.164"
1343 1343
1344 1344 xml:space="preserve"
1345 1345 font-family="{font_family}"
1346 1346 style="background:{background};" >
1347 1347
1348 1348 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1349 1349 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1350 1350 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1351 1351 168.596,153.916,216.671,
1352 1352 204.583,216.671z" fill="{text_color}"/>
1353 1353 <path d="M407.164,374.717L360.88,
1354 1354 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1355 1355 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1356 1356 15.366-44.203,23.488-69.076,23.488c-24.877,
1357 1357 0-48.762-8.122-69.078-23.488
1358 1358 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1359 1359 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1360 1360 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1361 1361 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1362 1362 19.402-10.527 C409.699,390.129,
1363 1363 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1364 1364 </svg>""".format(
1365 1365 size=self.size,
1366 1366 background='#979797', # @grey4
1367 1367 text_color=self.text_color,
1368 1368 font_family=font_family)
1369 1369
1370 1370 return {
1371 1371 "default_user": default_user
1372 1372 }[img_type]
1373 1373
1374 1374 def get_img_data(self, svg_type=None):
1375 1375 """
1376 1376 generates the svg metadata for image
1377 1377 """
1378 1378 fonts = [
1379 1379 '-apple-system',
1380 1380 'BlinkMacSystemFont',
1381 1381 'Segoe UI',
1382 1382 'Roboto',
1383 1383 'Oxygen-Sans',
1384 1384 'Ubuntu',
1385 1385 'Cantarell',
1386 1386 'Helvetica Neue',
1387 1387 'sans-serif'
1388 1388 ]
1389 1389 font_family = ','.join(fonts)
1390 1390 if svg_type:
1391 1391 return self.get_img_data_by_type(font_family, svg_type)
1392 1392
1393 1393 initials = self.get_initials()
1394 1394 img_data = """
1395 1395 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1396 1396 width="{size}" height="{size}"
1397 1397 style="width: 100%; height: 100%; background-color: {background}"
1398 1398 viewBox="0 0 {size} {size}">
1399 1399 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1400 1400 pointer-events="auto" fill="{text_color}"
1401 1401 font-family="{font_family}"
1402 1402 style="font-weight: 400; font-size: {f_size}px;">{text}
1403 1403 </text>
1404 1404 </svg>""".format(
1405 1405 size=self.size,
1406 1406 f_size=self.size/2.05, # scale the text inside the box nicely
1407 1407 background=self.background,
1408 1408 text_color=self.text_color,
1409 1409 text=initials.upper(),
1410 1410 font_family=font_family)
1411 1411
1412 1412 return img_data
1413 1413
1414 1414 def generate_svg(self, svg_type=None):
1415 1415 img_data = base64_to_str(self.get_img_data(svg_type))
1416 1416 return "data:image/svg+xml;base64,{}".format(img_data)
1417 1417
1418 1418
1419 def initials_gravatar(request, email_address, first_name, last_name, size=30, store_on_disk=False):
1419 def initials_gravatar(request, email_address, first_name, last_name, size=30):
1420 1420
1421 1421 svg_type = None
1422 1422 if email_address == User.DEFAULT_USER_EMAIL:
1423 1423 svg_type = 'default_user'
1424 1424
1425 1425 klass = InitialsGravatar(email_address, first_name, last_name, size)
1426
1427 if store_on_disk:
1428 from rhodecode.apps.file_store import utils as store_utils
1429 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
1430 FileOverSizeException
1431 from rhodecode.model.db import Session
1432
1433 image_key = md5_safe(email_address.lower()
1434 + first_name.lower() + last_name.lower())
1435
1436 storage = store_utils.get_file_storage(request.registry.settings)
1437 filename = '{}.svg'.format(image_key)
1438 subdir = 'gravatars'
1439 # since final name has a counter, we apply the 0
1440 uid = storage.apply_counter(0, store_utils.uid_filename(filename, randomized=False))
1441 store_uid = os.path.join(subdir, uid)
1442
1443 db_entry = FileStore.get_by_store_uid(store_uid)
1444 if db_entry:
1445 return request.route_path('download_file', fid=store_uid)
1446
1447 img_data = klass.get_img_data(svg_type=svg_type)
1448 img_file = store_utils.bytes_to_file_obj(img_data)
1449
1450 try:
1451 store_uid, metadata = storage.save_file(
1452 img_file, filename, directory=subdir,
1453 extensions=['.svg'], randomized_name=False)
1454 except (FileNotAllowedException, FileOverSizeException):
1455 raise
1456
1457 try:
1458 entry = FileStore.create(
1459 file_uid=store_uid, filename=metadata["filename"],
1460 file_hash=metadata["sha256"], file_size=metadata["size"],
1461 file_display_name=filename,
1462 file_description=f'user gravatar `{safe_str(filename)}`',
1463 hidden=True, check_acl=False, user_id=1
1464 )
1465 Session().add(entry)
1466 Session().commit()
1467 log.debug('Stored upload in DB as %s', entry)
1468 except Exception:
1469 raise
1470
1471 return request.route_path('download_file', fid=store_uid)
1472
1473 else:
1474 return klass.generate_svg(svg_type=svg_type)
1426 return klass.generate_svg(svg_type=svg_type)
1475 1427
1476 1428
1477 1429 def gravatar_external(request, gravatar_url_tmpl, email_address, size=30):
1478 1430 return safe_str(gravatar_url_tmpl)\
1479 1431 .replace('{email}', email_address) \
1480 1432 .replace('{md5email}', md5_safe(email_address.lower())) \
1481 1433 .replace('{netloc}', request.host) \
1482 1434 .replace('{scheme}', request.scheme) \
1483 1435 .replace('{size}', safe_str(size))
1484 1436
1485 1437
1486 1438 def gravatar_url(email_address, size=30, request=None):
1487 1439 request = request or get_current_request()
1488 1440 _use_gravatar = request.call_context.visual.use_gravatar
1489 1441
1490 1442 email_address = email_address or User.DEFAULT_USER_EMAIL
1491 1443 if isinstance(email_address, str):
1492 1444 # hashlib crashes on unicode items
1493 1445 email_address = safe_str(email_address)
1494 1446
1495 1447 # empty email or default user
1496 1448 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1497 1449 return initials_gravatar(request, User.DEFAULT_USER_EMAIL, '', '', size=size)
1498 1450
1499 1451 if _use_gravatar:
1500 1452 gravatar_url_tmpl = request.call_context.visual.gravatar_url \
1501 1453 or User.DEFAULT_GRAVATAR_URL
1502 1454 return gravatar_external(request, gravatar_url_tmpl, email_address, size=size)
1503 1455
1504 1456 else:
1505 1457 return initials_gravatar(request, email_address, '', '', size=size)
1506 1458
1507 1459
1508 1460 def breadcrumb_repo_link(repo):
1509 1461 """
1510 1462 Makes a breadcrumbs path link to repo
1511 1463
1512 1464 ex::
1513 1465 group >> subgroup >> repo
1514 1466
1515 1467 :param repo: a Repository instance
1516 1468 """
1517 1469
1518 1470 path = [
1519 1471 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1520 1472 title='last change:{}'.format(format_date(group.last_commit_change)))
1521 1473 for group in repo.groups_with_parents
1522 1474 ] + [
1523 1475 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1524 1476 title='last change:{}'.format(format_date(repo.last_commit_change)))
1525 1477 ]
1526 1478
1527 1479 return literal(' &raquo; '.join(path))
1528 1480
1529 1481
1530 1482 def breadcrumb_repo_group_link(repo_group):
1531 1483 """
1532 1484 Makes a breadcrumbs path link to repo
1533 1485
1534 1486 ex::
1535 1487 group >> subgroup
1536 1488
1537 1489 :param repo_group: a Repository Group instance
1538 1490 """
1539 1491
1540 1492 path = [
1541 1493 link_to(group.name,
1542 1494 route_path('repo_group_home', repo_group_name=group.group_name),
1543 1495 title='last change:{}'.format(format_date(group.last_commit_change)))
1544 1496 for group in repo_group.parents
1545 1497 ] + [
1546 1498 link_to(repo_group.name,
1547 1499 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1548 1500 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1549 1501 ]
1550 1502
1551 1503 return literal(' &raquo; '.join(path))
1552 1504
1553 1505
1554 1506 def format_byte_size_binary(file_size):
1555 1507 """
1556 1508 Formats file/folder sizes to standard.
1557 1509 """
1558 1510 if file_size is None:
1559 1511 file_size = 0
1560 1512
1561 1513 formatted_size = format_byte_size(file_size, binary=True)
1562 1514 return formatted_size
1563 1515
1564 1516
1565 1517 def urlify_text(text_, safe=True, **href_attrs):
1566 1518 """
1567 1519 Extract urls from text and make html links out of them
1568 1520 """
1569 1521
1570 1522 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1571 1523 r'''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1572 1524
1573 1525 def url_func(match_obj):
1574 1526 url_full = match_obj.groups()[0]
1575 1527 a_options = dict(href_attrs)
1576 1528 a_options['href'] = url_full
1577 1529 a_text = url_full
1578 1530 return HTML.tag("a", a_text, **a_options)
1579 1531
1580 1532 _new_text = url_pat.sub(url_func, text_)
1581 1533
1582 1534 if safe:
1583 1535 return literal(_new_text)
1584 1536 return _new_text
1585 1537
1586 1538
1587 1539 def urlify_commits(text_, repo_name):
1588 1540 """
1589 1541 Extract commit ids from text and make link from them
1590 1542
1591 1543 :param text_:
1592 1544 :param repo_name: repo name to build the URL with
1593 1545 """
1594 1546
1595 1547 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1596 1548
1597 1549 def url_func(match_obj):
1598 1550 commit_id = match_obj.groups()[1]
1599 1551 pref = match_obj.groups()[0]
1600 1552 suf = match_obj.groups()[2]
1601 1553
1602 1554 tmpl = (
1603 1555 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1604 1556 '%(commit_id)s</a>%(suf)s'
1605 1557 )
1606 1558 return tmpl % {
1607 1559 'pref': pref,
1608 1560 'cls': 'revision-link',
1609 1561 'url': route_url(
1610 1562 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1611 1563 'commit_id': commit_id,
1612 1564 'suf': suf,
1613 1565 'hovercard_alt': 'Commit: {}'.format(commit_id),
1614 1566 'hovercard_url': route_url(
1615 1567 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1616 1568 }
1617 1569
1618 1570 new_text = url_pat.sub(url_func, text_)
1619 1571
1620 1572 return new_text
1621 1573
1622 1574
1623 1575 def _process_url_func(match_obj, repo_name, uid, entry,
1624 1576 return_raw_data=False, link_format='html'):
1625 1577 pref = ''
1626 1578 if match_obj.group().startswith(' '):
1627 1579 pref = ' '
1628 1580
1629 1581 issue_id = ''.join(match_obj.groups())
1630 1582
1631 1583 if link_format == 'html':
1632 1584 tmpl = (
1633 1585 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1634 1586 '%(issue-prefix)s%(id-repr)s'
1635 1587 '</a>')
1636 1588 elif link_format == 'html+hovercard':
1637 1589 tmpl = (
1638 1590 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1639 1591 '%(issue-prefix)s%(id-repr)s'
1640 1592 '</a>')
1641 1593 elif link_format in ['rst', 'rst+hovercard']:
1642 1594 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1643 1595 elif link_format in ['markdown', 'markdown+hovercard']:
1644 1596 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1645 1597 else:
1646 1598 raise ValueError('Bad link_format:{}'.format(link_format))
1647 1599
1648 1600 (repo_name_cleaned,
1649 1601 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1650 1602
1651 1603 # variables replacement
1652 1604 named_vars = {
1653 1605 'id': issue_id,
1654 1606 'repo': repo_name,
1655 1607 'repo_name': repo_name_cleaned,
1656 1608 'group_name': parent_group_name,
1657 1609 # set dummy keys so we always have them
1658 1610 'hostname': '',
1659 1611 'netloc': '',
1660 1612 'scheme': ''
1661 1613 }
1662 1614
1663 1615 request = get_current_request()
1664 1616 if request:
1665 1617 # exposes, hostname, netloc, scheme
1666 1618 host_data = get_host_info(request)
1667 1619 named_vars.update(host_data)
1668 1620
1669 1621 # named regex variables
1670 1622 named_vars.update(match_obj.groupdict())
1671 1623 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1672 1624 desc = string.Template(escape(entry['desc'])).safe_substitute(**named_vars)
1673 1625 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1674 1626
1675 1627 def quote_cleaner(input_str):
1676 1628 """Remove quotes as it's HTML"""
1677 1629 return input_str.replace('"', '')
1678 1630
1679 1631 data = {
1680 1632 'pref': pref,
1681 1633 'cls': quote_cleaner('issue-tracker-link'),
1682 1634 'url': quote_cleaner(_url),
1683 1635 'id-repr': issue_id,
1684 1636 'issue-prefix': entry['pref'],
1685 1637 'serv': entry['url'],
1686 1638 'title': sanitize_html(desc, strip=True),
1687 1639 'hovercard_url': hovercard_url
1688 1640 }
1689 1641
1690 1642 if return_raw_data:
1691 1643 return {
1692 1644 'id': issue_id,
1693 1645 'url': _url
1694 1646 }
1695 1647 return tmpl % data
1696 1648
1697 1649
1698 1650 def get_active_pattern_entries(repo_name):
1699 1651 repo = None
1700 1652 if repo_name:
1701 1653 # Retrieving repo_name to avoid invalid repo_name to explode on
1702 1654 # IssueTrackerSettingsModel but still passing invalid name further down
1703 1655 repo = Repository.get_by_repo_name(repo_name, cache=True)
1704 1656
1705 1657 settings_model = IssueTrackerSettingsModel(repo=repo)
1706 1658 active_entries = settings_model.get_settings(cache=True)
1707 1659 return active_entries
1708 1660
1709 1661
1710 1662 pr_pattern_re = regex.compile(r'(?:(?:^!)|(?: !))(\d+)')
1711 1663
1712 1664 allowed_link_formats = [
1713 1665 'html', 'rst', 'markdown', 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1714 1666
1715 1667 compile_cache = {
1716 1668
1717 1669 }
1718 1670
1719 1671
1720 1672 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1721 1673
1722 1674 if link_format not in allowed_link_formats:
1723 1675 raise ValueError('Link format can be only one of:{} got {}'.format(
1724 1676 allowed_link_formats, link_format))
1725 1677 issues_data = []
1726 1678 errors = []
1727 1679 new_text = text_string
1728 1680
1729 1681 if active_entries is None:
1730 1682 log.debug('Fetch active issue tracker patterns for repo: %s', repo_name)
1731 1683 active_entries = get_active_pattern_entries(repo_name)
1732 1684
1733 1685 log.debug('Got %s pattern entries to process', len(active_entries))
1734 1686
1735 1687 for uid, entry in list(active_entries.items()):
1736 1688
1737 1689 if not (entry['pat'] and entry['url']):
1738 1690 log.debug('skipping due to missing data')
1739 1691 continue
1740 1692
1741 1693 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1742 1694 uid, entry['pat'], entry['url'], entry['pref'])
1743 1695
1744 1696 if entry.get('pat_compiled'):
1745 1697 pattern = entry['pat_compiled']
1746 1698 elif entry['pat'] in compile_cache:
1747 1699 pattern = compile_cache[entry['pat']]
1748 1700 else:
1749 1701 try:
1750 1702 pattern = regex.compile(r'%s' % entry['pat'])
1751 1703 except regex.error as e:
1752 1704 regex_err = ValueError('{}:{}'.format(entry['pat'], e))
1753 1705 log.exception('issue tracker pattern: `%s` failed to compile', regex_err)
1754 1706 errors.append(regex_err)
1755 1707 continue
1756 1708 compile_cache[entry['pat']] = pattern
1757 1709
1758 1710 data_func = partial(
1759 1711 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1760 1712 return_raw_data=True)
1761 1713
1762 1714 for match_obj in pattern.finditer(text_string):
1763 1715 issues_data.append(data_func(match_obj))
1764 1716
1765 1717 url_func = partial(
1766 1718 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1767 1719 link_format=link_format)
1768 1720
1769 1721 new_text = pattern.sub(url_func, new_text)
1770 1722 log.debug('processed prefix:uid `%s`', uid)
1771 1723
1772 1724 # finally use global replace, eg !123 -> pr-link, those will not catch
1773 1725 # if already similar pattern exists
1774 1726 server_url = '${scheme}://${netloc}'
1775 1727 pr_entry = {
1776 1728 'pref': '!',
1777 1729 'url': server_url + '/_admin/pull-requests/${id}',
1778 1730 'desc': 'Pull Request !${id}',
1779 1731 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1780 1732 }
1781 1733 pr_url_func = partial(
1782 1734 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1783 1735 link_format=link_format+'+hovercard')
1784 1736 new_text = pr_pattern_re.sub(pr_url_func, new_text)
1785 1737 log.debug('processed !pr pattern')
1786 1738
1787 1739 return new_text, issues_data, errors
1788 1740
1789 1741
1790 1742 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None,
1791 1743 issues_container_callback=None, error_container=None):
1792 1744 """
1793 1745 Parses given text message and makes proper links.
1794 1746 issues are linked to given issue-server, and rest is a commit link
1795 1747 """
1796 1748
1797 1749 def escaper(_text):
1798 1750 return _text.replace('<', '&lt;').replace('>', '&gt;')
1799 1751
1800 1752 new_text = escaper(commit_text)
1801 1753
1802 1754 # extract http/https links and make them real urls
1803 1755 new_text = urlify_text(new_text, safe=False)
1804 1756
1805 1757 # urlify commits - extract commit ids and make link out of them, if we have
1806 1758 # the scope of repository present.
1807 1759 if repository:
1808 1760 new_text = urlify_commits(new_text, repository)
1809 1761
1810 1762 # process issue tracker patterns
1811 1763 new_text, issues, errors = process_patterns(
1812 1764 new_text, repository or '', active_entries=active_pattern_entries)
1813 1765
1814 1766 if issues_container_callback is not None:
1815 1767 for issue in issues:
1816 1768 issues_container_callback(issue)
1817 1769
1818 1770 if error_container is not None:
1819 1771 error_container.extend(errors)
1820 1772
1821 1773 return literal(new_text)
1822 1774
1823 1775
1824 1776 def render_binary(repo_name, file_obj):
1825 1777 """
1826 1778 Choose how to render a binary file
1827 1779 """
1828 1780
1829 1781 # unicode
1830 1782 filename = file_obj.name
1831 1783
1832 1784 # images
1833 1785 for ext in ['*.png', '*.jpeg', '*.jpg', '*.ico', '*.gif']:
1834 1786 if fnmatch.fnmatch(filename, pat=ext):
1835 1787 src = route_path(
1836 1788 'repo_file_raw', repo_name=repo_name,
1837 1789 commit_id=file_obj.commit.raw_id,
1838 1790 f_path=file_obj.path)
1839 1791
1840 1792 return literal(
1841 1793 '<img class="rendered-binary" alt="rendered-image" src="{}">'.format(src))
1842 1794
1843 1795
1844 1796 def renderer_from_filename(filename, exclude=None):
1845 1797 """
1846 1798 choose a renderer based on filename, this works only for text based files
1847 1799 """
1848 1800
1849 1801 # ipython
1850 1802 for ext in ['*.ipynb']:
1851 1803 if fnmatch.fnmatch(filename, pat=ext):
1852 1804 return 'jupyter'
1853 1805
1854 1806 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1855 1807 if is_markup:
1856 1808 return is_markup
1857 1809 return None
1858 1810
1859 1811
1860 1812 def render(source, renderer='rst', mentions=False, relative_urls=None,
1861 1813 repo_name=None, active_pattern_entries=None, issues_container_callback=None):
1862 1814
1863 1815 def maybe_convert_relative_links(html_source):
1864 1816 if relative_urls:
1865 1817 return relative_links(html_source, relative_urls)
1866 1818 return html_source
1867 1819
1868 1820 if renderer == 'plain':
1869 1821 return literal(
1870 1822 MarkupRenderer.plain(source, leading_newline=False))
1871 1823
1872 1824 elif renderer == 'rst':
1873 1825 if repo_name:
1874 1826 # process patterns on comments if we pass in repo name
1875 1827 source, issues, errors = process_patterns(
1876 1828 source, repo_name, link_format='rst',
1877 1829 active_entries=active_pattern_entries)
1878 1830 if issues_container_callback is not None:
1879 1831 for issue in issues:
1880 1832 issues_container_callback(issue)
1881 1833
1882 1834 rendered_block = maybe_convert_relative_links(
1883 1835 MarkupRenderer.rst(source, mentions=mentions))
1884 1836
1885 1837 return literal(f'<div class="rst-block">{rendered_block}</div>')
1886 1838
1887 1839 elif renderer == 'markdown':
1888 1840 if repo_name:
1889 1841 # process patterns on comments if we pass in repo name
1890 1842 source, issues, errors = process_patterns(
1891 1843 source, repo_name, link_format='markdown',
1892 1844 active_entries=active_pattern_entries)
1893 1845 if issues_container_callback is not None:
1894 1846 for issue in issues:
1895 1847 issues_container_callback(issue)
1896 1848
1897 1849 rendered_block = maybe_convert_relative_links(
1898 1850 MarkupRenderer.markdown(source, flavored=True, mentions=mentions))
1899 1851 return literal(f'<div class="markdown-block">{rendered_block}</div>')
1900 1852
1901 1853 elif renderer == 'jupyter':
1902 1854 rendered_block = maybe_convert_relative_links(
1903 1855 MarkupRenderer.jupyter(source))
1904 1856 return literal(f'<div class="ipynb">{rendered_block}</div>')
1905 1857
1906 1858 # None means just show the file-source
1907 1859 return None
1908 1860
1909 1861
1910 1862 def commit_status(repo, commit_id):
1911 1863 return ChangesetStatusModel().get_status(repo, commit_id)
1912 1864
1913 1865
1914 1866 def commit_status_lbl(commit_status):
1915 1867 return dict(ChangesetStatus.STATUSES).get(commit_status)
1916 1868
1917 1869
1918 1870 def commit_time(repo_name, commit_id):
1919 1871 repo = Repository.get_by_repo_name(repo_name)
1920 1872 commit = repo.get_commit(commit_id=commit_id)
1921 1873 return commit.date
1922 1874
1923 1875
1924 1876 def get_permission_name(key):
1925 1877 return dict(Permission.PERMS).get(key)
1926 1878
1927 1879
1928 1880 def journal_filter_help(request):
1929 1881 _ = request.translate
1930 1882 from rhodecode.lib.audit_logger import ACTIONS
1931 1883 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1932 1884
1933 1885 return _(
1934 1886 'Example filter terms:\n' +
1935 1887 ' repository:vcs\n' +
1936 1888 ' username:marcin\n' +
1937 1889 ' username:(NOT marcin)\n' +
1938 1890 ' action:*push*\n' +
1939 1891 ' ip:127.0.0.1\n' +
1940 1892 ' date:20120101\n' +
1941 1893 ' date:[20120101100000 TO 20120102]\n' +
1942 1894 '\n' +
1943 1895 'Actions: {actions}\n' +
1944 1896 '\n' +
1945 1897 'Generate wildcards using \'*\' character:\n' +
1946 1898 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1947 1899 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1948 1900 '\n' +
1949 1901 'Optional AND / OR operators in queries\n' +
1950 1902 ' "repository:vcs OR repository:test"\n' +
1951 1903 ' "username:test AND repository:test*"\n'
1952 1904 ).format(actions=actions)
1953 1905
1954 1906
1955 1907 def not_mapped_error(repo_name):
1956 1908 from rhodecode.translation import _
1957 1909 flash(_('%s repository is not mapped to db perhaps'
1958 1910 ' it was created or renamed from the filesystem'
1959 1911 ' please run the application again'
1960 1912 ' in order to rescan repositories') % repo_name, category='error')
1961 1913
1962 1914
1963 1915 def ip_range(ip_addr):
1964 1916 from rhodecode.model.db import UserIpMap
1965 1917 s, e = UserIpMap._get_ip_range(ip_addr)
1966 1918 return '%s - %s' % (s, e)
1967 1919
1968 1920
1969 1921 def form(url, method='post', needs_csrf_token=True, **attrs):
1970 1922 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1971 1923 if method.lower() != 'get' and needs_csrf_token:
1972 1924 raise Exception(
1973 1925 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1974 1926 'CSRF token. If the endpoint does not require such token you can ' +
1975 1927 'explicitly set the parameter needs_csrf_token to false.')
1976 1928
1977 1929 return insecure_form(url, method=method, **attrs)
1978 1930
1979 1931
1980 1932 def secure_form(form_url, method="POST", multipart=False, **attrs):
1981 1933 """Start a form tag that points the action to an url. This
1982 1934 form tag will also include the hidden field containing
1983 1935 the auth token.
1984 1936
1985 1937 The url options should be given either as a string, or as a
1986 1938 ``url()`` function. The method for the form defaults to POST.
1987 1939
1988 1940 Options:
1989 1941
1990 1942 ``multipart``
1991 1943 If set to True, the enctype is set to "multipart/form-data".
1992 1944 ``method``
1993 1945 The method to use when submitting the form, usually either
1994 1946 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1995 1947 hidden input with name _method is added to simulate the verb
1996 1948 over POST.
1997 1949
1998 1950 """
1999 1951
2000 1952 if 'request' in attrs:
2001 1953 session = attrs['request'].session
2002 1954 del attrs['request']
2003 1955 else:
2004 1956 raise ValueError(
2005 1957 'Calling this form requires request= to be passed as argument')
2006 1958
2007 1959 _form = insecure_form(form_url, method, multipart, **attrs)
2008 1960 token = literal(
2009 1961 '<input type="hidden" name="{}" value="{}">'.format(
2010 1962 csrf_token_key, get_csrf_token(session)))
2011 1963
2012 1964 return literal("%s\n%s" % (_form, token))
2013 1965
2014 1966
2015 1967 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
2016 1968 select_html = select(name, selected, options, **attrs)
2017 1969
2018 1970 select2 = """
2019 1971 <script>
2020 1972 $(document).ready(function() {
2021 1973 $('#%s').select2({
2022 1974 containerCssClass: 'drop-menu %s',
2023 1975 dropdownCssClass: 'drop-menu-dropdown',
2024 1976 dropdownAutoWidth: true%s
2025 1977 });
2026 1978 });
2027 1979 </script>
2028 1980 """
2029 1981
2030 1982 filter_option = """,
2031 1983 minimumResultsForSearch: -1
2032 1984 """
2033 1985 input_id = attrs.get('id') or name
2034 1986 extra_classes = ' '.join(attrs.pop('extra_classes', []))
2035 1987 filter_enabled = "" if enable_filter else filter_option
2036 1988 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
2037 1989
2038 1990 return literal(select_html+select_script)
2039 1991
2040 1992
2041 1993 def get_visual_attr(tmpl_context_var, attr_name):
2042 1994 """
2043 1995 A safe way to get a variable from visual variable of template context
2044 1996
2045 1997 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
2046 1998 :param attr_name: name of the attribute we fetch from the c.visual
2047 1999 """
2048 2000 visual = getattr(tmpl_context_var, 'visual', None)
2049 2001 if not visual:
2050 2002 return
2051 2003 else:
2052 2004 return getattr(visual, attr_name, None)
2053 2005
2054 2006
2055 2007 def get_last_path_part(file_node):
2056 2008 if not file_node.path:
2057 2009 return '/'
2058 2010
2059 2011 path = safe_str(file_node.path.split('/')[-1])
2060 2012 return '../' + path
2061 2013
2062 2014
2063 2015 def route_url(*args, **kwargs):
2064 2016 """
2065 2017 Wrapper around pyramids `route_url` (fully qualified url) function.
2066 2018 """
2067 2019 req = get_current_request()
2068 2020 return req.route_url(*args, **kwargs)
2069 2021
2070 2022
2071 2023 def route_path(*args, **kwargs):
2072 2024 """
2073 2025 Wrapper around pyramids `route_path` function.
2074 2026 """
2075 2027 req = get_current_request()
2076 2028 return req.route_path(*args, **kwargs)
2077 2029
2078 2030
2079 2031 def route_path_or_none(*args, **kwargs):
2080 2032 try:
2081 2033 return route_path(*args, **kwargs)
2082 2034 except KeyError:
2083 2035 return None
2084 2036
2085 2037
2086 2038 def current_route_path(request, **kw):
2087 2039 new_args = request.GET.mixed()
2088 2040 new_args.update(kw)
2089 2041 return request.current_route_path(_query=new_args)
2090 2042
2091 2043
2092 2044 def curl_api_example(method, args):
2093 2045 args_json = json.dumps(OrderedDict([
2094 2046 ('id', 1),
2095 2047 ('auth_token', 'SECRET'),
2096 2048 ('method', method),
2097 2049 ('args', args)
2098 2050 ]))
2099 2051
2100 2052 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
2101 2053 api_url=route_url('apiv2'),
2102 2054 args_json=args_json
2103 2055 )
2104 2056
2105 2057
2106 2058 def api_call_example(method, args):
2107 2059 """
2108 2060 Generates an API call example via CURL
2109 2061 """
2110 2062 curl_call = curl_api_example(method, args)
2111 2063
2112 2064 return literal(
2113 2065 curl_call +
2114 2066 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2115 2067 "and needs to be of `api calls` role."
2116 2068 .format(token_url=route_url('my_account_auth_tokens')))
2117 2069
2118 2070
2119 2071 def notification_description(notification, request):
2120 2072 """
2121 2073 Generate notification human readable description based on notification type
2122 2074 """
2123 2075 from rhodecode.model.notification import NotificationModel
2124 2076 return NotificationModel().make_description(
2125 2077 notification, translate=request.translate)
2126 2078
2127 2079
2128 2080 def go_import_header(request, db_repo=None):
2129 2081 """
2130 2082 Creates a header for go-import functionality in Go Lang
2131 2083 """
2132 2084
2133 2085 if not db_repo:
2134 2086 return
2135 2087 if 'go-get' not in request.GET:
2136 2088 return
2137 2089
2138 2090 clone_url = db_repo.clone_url()
2139 2091 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2140 2092 # we have a repo and go-get flag,
2141 2093 return literal('<meta name="go-import" content="{} {} {}">'.format(
2142 2094 prefix, db_repo.repo_type, clone_url))
2143 2095
2144 2096
2145 2097 def reviewer_as_json(*args, **kwargs):
2146 2098 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2147 2099 return _reviewer_as_json(*args, **kwargs)
2148 2100
2149 2101
2150 2102 def get_repo_view_type(request):
2151 2103 route_name = request.matched_route.name
2152 2104 route_to_view_type = {
2153 2105 'repo_changelog': 'commits',
2154 2106 'repo_commits': 'commits',
2155 2107 'repo_files': 'files',
2156 2108 'repo_summary': 'summary',
2157 2109 'repo_commit': 'commit'
2158 2110 }
2159 2111
2160 2112 return route_to_view_type.get(route_name)
2161 2113
2162 2114
2163 2115 def is_active(menu_entry, selected):
2164 2116 """
2165 2117 Returns active class for selecting menus in templates
2166 2118 <li class=${h.is_active('settings', current_active)}></li>
2167 2119 """
2168 2120 if not isinstance(menu_entry, list):
2169 2121 menu_entry = [menu_entry]
2170 2122
2171 2123 if selected in menu_entry:
2172 2124 return "active"
2173 2125
2174 2126
2175 2127 class IssuesRegistry(object):
2176 2128 """
2177 2129 issue_registry = IssuesRegistry()
2178 2130 some_func(issues_callback=issues_registry(...))
2179 2131 """
2180 2132
2181 2133 def __init__(self):
2182 2134 self.issues = []
2183 2135 self.unique_issues = collections.defaultdict(lambda: [])
2184 2136
2185 2137 def __call__(self, commit_dict=None):
2186 2138 def callback(issue):
2187 2139 if commit_dict and issue:
2188 2140 issue['commit'] = commit_dict
2189 2141 self.issues.append(issue)
2190 2142 self.unique_issues[issue['id']].append(issue)
2191 2143 return callback
2192 2144
2193 2145 def get_issues(self):
2194 2146 return self.issues
2195 2147
2196 2148 @property
2197 2149 def issues_unique_count(self):
2198 2150 return len(set(i['id'] for i in self.issues))
2199 2151
2200 2152
2201 2153 def get_directory_statistics(start_path):
2202 2154 """
2203 2155 total_files, total_size, directory_stats = get_directory_statistics(start_path)
2204 2156
2205 2157 print(f"Directory statistics for: {start_path}\n")
2206 2158 print(f"Total files: {total_files}")
2207 2159 print(f"Total size: {format_size(total_size)}\n")
2208 2160
2209 2161 :param start_path:
2210 2162 :return:
2211 2163 """
2212 2164
2213 2165 total_files = 0
2214 2166 total_size = 0
2215 2167 directory_stats = {}
2216 2168
2217 2169 for dir_path, dir_names, file_names in os.walk(start_path):
2218 2170 dir_size = 0
2219 2171 file_count = len(file_names)
2220 2172
2221 2173 for fname in file_names:
2222 2174 filepath = os.path.join(dir_path, fname)
2223 2175 file_size = os.path.getsize(filepath)
2224 2176 dir_size += file_size
2225 2177
2226 2178 directory_stats[dir_path] = {'file_count': file_count, 'size': dir_size}
2227 2179 total_files += file_count
2228 2180 total_size += dir_size
2229 2181
2230 2182 return total_files, total_size, directory_stats
@@ -1,104 +1,104 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import logging
20 20 import traceback
21 21
22 22 from rhodecode.model import meta
23 23
24 24 from rhodecode.lib import hooks_base
25 25 from rhodecode.lib.exceptions import HTTPLockedRC, HTTPBranchProtected
26 26 from rhodecode.lib.utils2 import AttributeDict
27 27
28 28 log = logging.getLogger(__name__)
29 29
30 30
31 31 class Hooks(object):
32 32 """
33 33 Exposes the hooks for remote callbacks
34 34 """
35 35 def __init__(self, request=None, log_prefix=''):
36 36 self.log_prefix = log_prefix
37 37 self.request = request
38 38
39 39 def repo_size(self, extras):
40 40 log.debug("%sCalled repo_size of %s object", self.log_prefix, self)
41 41 return self._call_hook(hooks_base.repo_size, extras)
42 42
43 43 def pre_pull(self, extras):
44 44 log.debug("%sCalled pre_pull of %s object", self.log_prefix, self)
45 45 return self._call_hook(hooks_base.pre_pull, extras)
46 46
47 47 def post_pull(self, extras):
48 48 log.debug("%sCalled post_pull of %s object", self.log_prefix, self)
49 49 return self._call_hook(hooks_base.post_pull, extras)
50 50
51 51 def pre_push(self, extras):
52 52 log.debug("%sCalled pre_push of %s object", self.log_prefix, self)
53 53 return self._call_hook(hooks_base.pre_push, extras)
54 54
55 55 def post_push(self, extras):
56 56 log.debug("%sCalled post_push of %s object", self.log_prefix, self)
57 57 return self._call_hook(hooks_base.post_push, extras)
58 58
59 59 def _call_hook(self, hook, extras):
60 60 extras = AttributeDict(extras)
61 61 _server_url = extras['server_url']
62 62
63 63 extras.request = self.request
64 64
65 65 try:
66 66 result = hook(extras)
67 67 if result is None:
68 68 raise Exception(f'Failed to obtain hook result from func: {hook}')
69 except HTTPBranchProtected as handled_error:
69 except HTTPBranchProtected as error:
70 70 # Those special cases don't need error reporting. It's a case of
71 71 # locked repo or protected branch
72 72 result = AttributeDict({
73 'status': handled_error.code,
74 'output': handled_error.explanation
73 'status': error.code,
74 'output': error.explanation
75 75 })
76 76 except (HTTPLockedRC, Exception) as error:
77 77 # locked needs different handling since we need to also
78 78 # handle PULL operations
79 79 exc_tb = ''
80 80 if not isinstance(error, HTTPLockedRC):
81 81 exc_tb = traceback.format_exc()
82 82 log.exception('%sException when handling hook %s', self.log_prefix, hook)
83 83 error_args = error.args
84 84 return {
85 85 'status': 128,
86 86 'output': '',
87 87 'exception': type(error).__name__,
88 88 'exception_traceback': exc_tb,
89 89 'exception_args': error_args,
90 90 }
91 91 finally:
92 92 meta.Session.remove()
93 93
94 94 log.debug('%sGot hook call response %s', self.log_prefix, result)
95 95 return {
96 96 'status': result.status,
97 97 'output': result.output,
98 98 }
99 99
100 100 def __enter__(self):
101 101 return self
102 102
103 103 def __exit__(self, exc_type, exc_val, exc_tb):
104 104 pass
@@ -1,534 +1,550 b''
1 1 # Copyright (C) 2013-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 """
21 21 Set of hooks run by RhodeCode Enterprise
22 22 """
23 23
24 24 import os
25 25 import logging
26 26
27 27 import rhodecode
28 28 from rhodecode import events
29 29 from rhodecode.lib import helpers as h
30 30 from rhodecode.lib import audit_logger
31 31 from rhodecode.lib.utils2 import safe_str, user_agent_normalizer
32 32 from rhodecode.lib.exceptions import (
33 HTTPLockedRC, HTTPBranchProtected, UserCreationError)
33 HTTPLockedRC, HTTPBranchProtected, UserCreationError, ClientNotSupportedError)
34 34 from rhodecode.model.db import Repository, User
35 35 from rhodecode.lib.statsd_client import StatsdClient
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 class HookResponse(object):
41 41 def __init__(self, status, output):
42 42 self.status = status
43 43 self.output = output
44 44
45 45 def __add__(self, other):
46 46 other_status = getattr(other, 'status', 0)
47 47 new_status = max(self.status, other_status)
48 48 other_output = getattr(other, 'output', '')
49 49 new_output = self.output + other_output
50 50
51 51 return HookResponse(new_status, new_output)
52 52
53 53 def __bool__(self):
54 54 return self.status == 0
55 55
56 56 def to_json(self):
57 57 return {'status': self.status, 'output': self.output}
58 58
59 59
60 60 def is_shadow_repo(extras):
61 61 """
62 62 Returns ``True`` if this is an action executed against a shadow repository.
63 63 """
64 64 return extras['is_shadow_repo']
65 65
66 66
67 def check_vcs_client(extras):
68 """
69 Checks if vcs client is allowed (Only works in enterprise edition)
70 """
71 try:
72 from rc_ee.lib.security.utils import is_vcs_client_whitelisted
73 except ModuleNotFoundError:
74 is_vcs_client_whitelisted = lambda *x: True
75 backend = extras.get('scm')
76 if not is_vcs_client_whitelisted(extras.get('user_agent'), backend):
77 raise ClientNotSupportedError(f"Your {backend} client is forbidden")
78
67 79 def _get_scm_size(alias, root_path):
68 80
69 81 if not alias.startswith('.'):
70 82 alias += '.'
71 83
72 84 size_scm, size_root = 0, 0
73 85 for path, unused_dirs, files in os.walk(safe_str(root_path)):
74 86 if path.find(alias) != -1:
75 87 for f in files:
76 88 try:
77 89 size_scm += os.path.getsize(os.path.join(path, f))
78 90 except OSError:
79 91 pass
80 92 else:
81 93 for f in files:
82 94 try:
83 95 size_root += os.path.getsize(os.path.join(path, f))
84 96 except OSError:
85 97 pass
86 98
87 99 size_scm_f = h.format_byte_size_binary(size_scm)
88 100 size_root_f = h.format_byte_size_binary(size_root)
89 101 size_total_f = h.format_byte_size_binary(size_root + size_scm)
90 102
91 103 return size_scm_f, size_root_f, size_total_f
92 104
93 105
94 106 # actual hooks called by Mercurial internally, and GIT by our Python Hooks
95 107 def repo_size(extras):
96 108 """Present size of repository after push."""
97 109 repo = Repository.get_by_repo_name(extras.repository)
98 110 vcs_part = f'.{repo.repo_type}'
99 111 size_vcs, size_root, size_total = _get_scm_size(vcs_part, repo.repo_full_path)
100 112 msg = (f'RhodeCode: `{repo.repo_name}` size summary {vcs_part}:{size_vcs} repo:{size_root} total:{size_total}\n')
101 113 return HookResponse(0, msg)
102 114
103 115
104 116 def pre_push(extras):
105 117 """
106 118 Hook executed before pushing code.
107 119
108 120 It bans pushing when the repository is locked.
109 121 """
110 122
123 check_vcs_client(extras)
111 124 user = User.get_by_username(extras.username)
112 125 output = ''
113 126 if extras.locked_by[0] and user.user_id != int(extras.locked_by[0]):
114 127 locked_by = User.get(extras.locked_by[0]).username
115 128 reason = extras.locked_by[2]
116 129 # this exception is interpreted in git/hg middlewares and based
117 130 # on that proper return code is server to client
118 131 _http_ret = HTTPLockedRC(
119 132 _locked_by_explanation(extras.repository, locked_by, reason))
120 133 if str(_http_ret.code).startswith('2'):
121 134 # 2xx Codes don't raise exceptions
122 135 output = _http_ret.title
123 136 else:
124 137 raise _http_ret
125 138
126 139 hook_response = ''
127 140 if not is_shadow_repo(extras):
128 141
129 142 if extras.commit_ids and extras.check_branch_perms:
130 143 auth_user = user.AuthUser()
131 144 repo = Repository.get_by_repo_name(extras.repository)
145 if not repo:
146 raise ValueError(f'Repo for {extras.repository} not found')
132 147 affected_branches = []
133 148 if repo.repo_type == 'hg':
134 149 for entry in extras.commit_ids:
135 150 if entry['type'] == 'branch':
136 151 is_forced = bool(entry['multiple_heads'])
137 152 affected_branches.append([entry['name'], is_forced])
138 153 elif repo.repo_type == 'git':
139 154 for entry in extras.commit_ids:
140 155 if entry['type'] == 'heads':
141 156 is_forced = bool(entry['pruned_sha'])
142 157 affected_branches.append([entry['name'], is_forced])
143 158
144 159 for branch_name, is_forced in affected_branches:
145 160
146 161 rule, branch_perm = auth_user.get_rule_and_branch_permission(
147 162 extras.repository, branch_name)
148 163 if not branch_perm:
149 164 # no branch permission found for this branch, just keep checking
150 165 continue
151 166
152 167 if branch_perm == 'branch.push_force':
153 168 continue
154 169 elif branch_perm == 'branch.push' and is_forced is False:
155 170 continue
156 171 elif branch_perm == 'branch.push' and is_forced is True:
157 172 halt_message = f'Branch `{branch_name}` changes rejected by rule {rule}. ' \
158 173 f'FORCE PUSH FORBIDDEN.'
159 174 else:
160 175 halt_message = f'Branch `{branch_name}` changes rejected by rule {rule}.'
161 176
162 177 if halt_message:
163 178 _http_ret = HTTPBranchProtected(halt_message)
164 179 raise _http_ret
165 180
166 181 # Propagate to external components. This is done after checking the
167 182 # lock, for consistent behavior.
168 183 hook_response = pre_push_extension(
169 184 repo_store_path=Repository.base_path(), **extras)
170 185 events.trigger(events.RepoPrePushEvent(
171 186 repo_name=extras.repository, extras=extras))
172 187
173 188 return HookResponse(0, output) + hook_response
174 189
175 190
176 191 def pre_pull(extras):
177 192 """
178 193 Hook executed before pulling the code.
179 194
180 195 It bans pulling when the repository is locked.
181 196 """
182 197
198 check_vcs_client(extras)
183 199 output = ''
184 200 if extras.locked_by[0]:
185 201 locked_by = User.get(extras.locked_by[0]).username
186 202 reason = extras.locked_by[2]
187 203 # this exception is interpreted in git/hg middlewares and based
188 204 # on that proper return code is server to client
189 205 _http_ret = HTTPLockedRC(
190 206 _locked_by_explanation(extras.repository, locked_by, reason))
191 207 if str(_http_ret.code).startswith('2'):
192 208 # 2xx Codes don't raise exceptions
193 209 output = _http_ret.title
194 210 else:
195 211 raise _http_ret
196 212
197 213 # Propagate to external components. This is done after checking the
198 214 # lock, for consistent behavior.
199 215 hook_response = ''
200 216 if not is_shadow_repo(extras):
201 217 extras.hook_type = extras.hook_type or 'pre_pull'
202 218 hook_response = pre_pull_extension(
203 219 repo_store_path=Repository.base_path(), **extras)
204 220 events.trigger(events.RepoPrePullEvent(
205 221 repo_name=extras.repository, extras=extras))
206 222
207 223 return HookResponse(0, output) + hook_response
208 224
209 225
210 226 def post_pull(extras):
211 227 """Hook executed after client pulls the code."""
212 228
213 229 audit_user = audit_logger.UserWrap(
214 230 username=extras.username,
215 231 ip_addr=extras.ip)
216 232 repo = audit_logger.RepoWrap(repo_name=extras.repository)
217 233 audit_logger.store(
218 234 'user.pull', action_data={'user_agent': extras.user_agent},
219 235 user=audit_user, repo=repo, commit=True)
220 236
221 237 statsd = StatsdClient.statsd
222 238 if statsd:
223 239 statsd.incr('rhodecode_pull_total', tags=[
224 240 f'user-agent:{user_agent_normalizer(extras.user_agent)}',
225 241 ])
226 242 output = ''
227 243 # make lock is a tri state False, True, None. We only make lock on True
228 244 if extras.make_lock is True and not is_shadow_repo(extras):
229 245 user = User.get_by_username(extras.username)
230 246 Repository.lock(Repository.get_by_repo_name(extras.repository),
231 247 user.user_id,
232 248 lock_reason=Repository.LOCK_PULL)
233 249 msg = 'Made lock on repo `{}`'.format(extras.repository)
234 250 output += msg
235 251
236 252 if extras.locked_by[0]:
237 253 locked_by = User.get(extras.locked_by[0]).username
238 254 reason = extras.locked_by[2]
239 255 _http_ret = HTTPLockedRC(
240 256 _locked_by_explanation(extras.repository, locked_by, reason))
241 257 if str(_http_ret.code).startswith('2'):
242 258 # 2xx Codes don't raise exceptions
243 259 output += _http_ret.title
244 260
245 261 # Propagate to external components.
246 262 hook_response = ''
247 263 if not is_shadow_repo(extras):
248 264 extras.hook_type = extras.hook_type or 'post_pull'
249 265 hook_response = post_pull_extension(
250 266 repo_store_path=Repository.base_path(), **extras)
251 267 events.trigger(events.RepoPullEvent(
252 268 repo_name=extras.repository, extras=extras))
253 269
254 270 return HookResponse(0, output) + hook_response
255 271
256 272
257 273 def post_push(extras):
258 274 """Hook executed after user pushes to the repository."""
259 275 commit_ids = extras.commit_ids
260 276
261 277 # log the push call
262 278 audit_user = audit_logger.UserWrap(
263 279 username=extras.username, ip_addr=extras.ip)
264 280 repo = audit_logger.RepoWrap(repo_name=extras.repository)
265 281 audit_logger.store(
266 282 'user.push', action_data={
267 283 'user_agent': extras.user_agent,
268 284 'commit_ids': commit_ids[:400]},
269 285 user=audit_user, repo=repo, commit=True)
270 286
271 287 statsd = StatsdClient.statsd
272 288 if statsd:
273 289 statsd.incr('rhodecode_push_total', tags=[
274 290 f'user-agent:{user_agent_normalizer(extras.user_agent)}',
275 291 ])
276 292
277 293 # Propagate to external components.
278 294 output = ''
279 295 # make lock is a tri state False, True, None. We only release lock on False
280 296 if extras.make_lock is False and not is_shadow_repo(extras):
281 297 Repository.unlock(Repository.get_by_repo_name(extras.repository))
282 298 msg = f'Released lock on repo `{extras.repository}`\n'
283 299 output += msg
284 300
285 301 if extras.locked_by[0]:
286 302 locked_by = User.get(extras.locked_by[0]).username
287 303 reason = extras.locked_by[2]
288 304 _http_ret = HTTPLockedRC(
289 305 _locked_by_explanation(extras.repository, locked_by, reason))
290 306 # TODO: johbo: if not?
291 307 if str(_http_ret.code).startswith('2'):
292 308 # 2xx Codes don't raise exceptions
293 309 output += _http_ret.title
294 310
295 311 if extras.new_refs:
296 312 tmpl = '{}/{}/pull-request/new?{{ref_type}}={{ref_name}}'.format(
297 313 safe_str(extras.server_url), safe_str(extras.repository))
298 314
299 315 for branch_name in extras.new_refs['branches']:
300 316 pr_link = tmpl.format(ref_type='branch', ref_name=safe_str(branch_name))
301 317 output += f'RhodeCode: open pull request link: {pr_link}\n'
302 318
303 319 for book_name in extras.new_refs['bookmarks']:
304 320 pr_link = tmpl.format(ref_type='bookmark', ref_name=safe_str(book_name))
305 321 output += f'RhodeCode: open pull request link: {pr_link}\n'
306 322
307 323 hook_response = ''
308 324 if not is_shadow_repo(extras):
309 325 hook_response = post_push_extension(
310 326 repo_store_path=Repository.base_path(),
311 327 **extras)
312 328 events.trigger(events.RepoPushEvent(
313 329 repo_name=extras.repository, pushed_commit_ids=commit_ids, extras=extras))
314 330
315 331 output += 'RhodeCode: push completed\n'
316 332 return HookResponse(0, output) + hook_response
317 333
318 334
319 335 def _locked_by_explanation(repo_name, user_name, reason):
320 336 message = f'Repository `{repo_name}` locked by user `{user_name}`. Reason:`{reason}`'
321 337 return message
322 338
323 339
324 340 def check_allowed_create_user(user_dict, created_by, **kwargs):
325 341 # pre create hooks
326 342 if pre_create_user.is_active():
327 343 hook_result = pre_create_user(created_by=created_by, **user_dict)
328 344 allowed = hook_result.status == 0
329 345 if not allowed:
330 346 reason = hook_result.output
331 347 raise UserCreationError(reason)
332 348
333 349
334 350 class ExtensionCallback(object):
335 351 """
336 352 Forwards a given call to rcextensions, sanitizes keyword arguments.
337 353
338 354 Does check if there is an extension active for that hook. If it is
339 355 there, it will forward all `kwargs_keys` keyword arguments to the
340 356 extension callback.
341 357 """
342 358
343 359 def __init__(self, hook_name, kwargs_keys):
344 360 self._hook_name = hook_name
345 361 self._kwargs_keys = set(kwargs_keys)
346 362
347 363 def __call__(self, *args, **kwargs):
348 364 log.debug('Calling extension callback for `%s`', self._hook_name)
349 365 callback = self._get_callback()
350 366 if not callback:
351 367 log.debug('extension callback `%s` not found, skipping...', self._hook_name)
352 368 return
353 369
354 370 kwargs_to_pass = {}
355 371 for key in self._kwargs_keys:
356 372 try:
357 373 kwargs_to_pass[key] = kwargs[key]
358 374 except KeyError:
359 375 log.error('Failed to fetch %s key from given kwargs. '
360 376 'Expected keys: %s', key, self._kwargs_keys)
361 377 raise
362 378
363 379 # backward compat for removed api_key for old hooks. This was it works
364 380 # with older rcextensions that require api_key present
365 381 if self._hook_name in ['CREATE_USER_HOOK', 'DELETE_USER_HOOK']:
366 382 kwargs_to_pass['api_key'] = '_DEPRECATED_'
367 383 return callback(**kwargs_to_pass)
368 384
369 385 def is_active(self):
370 386 return hasattr(rhodecode.EXTENSIONS, self._hook_name)
371 387
372 388 def _get_callback(self):
373 389 return getattr(rhodecode.EXTENSIONS, self._hook_name, None)
374 390
375 391
376 392 pre_pull_extension = ExtensionCallback(
377 393 hook_name='PRE_PULL_HOOK',
378 394 kwargs_keys=(
379 395 'server_url', 'config', 'scm', 'username', 'ip', 'action',
380 396 'repository', 'hook_type', 'user_agent', 'repo_store_path',))
381 397
382 398
383 399 post_pull_extension = ExtensionCallback(
384 400 hook_name='PULL_HOOK',
385 401 kwargs_keys=(
386 402 'server_url', 'config', 'scm', 'username', 'ip', 'action',
387 403 'repository', 'hook_type', 'user_agent', 'repo_store_path',))
388 404
389 405
390 406 pre_push_extension = ExtensionCallback(
391 407 hook_name='PRE_PUSH_HOOK',
392 408 kwargs_keys=(
393 409 'server_url', 'config', 'scm', 'username', 'ip', 'action',
394 410 'repository', 'repo_store_path', 'commit_ids', 'hook_type', 'user_agent',))
395 411
396 412
397 413 post_push_extension = ExtensionCallback(
398 414 hook_name='PUSH_HOOK',
399 415 kwargs_keys=(
400 416 'server_url', 'config', 'scm', 'username', 'ip', 'action',
401 417 'repository', 'repo_store_path', 'commit_ids', 'hook_type', 'user_agent',))
402 418
403 419
404 420 pre_create_user = ExtensionCallback(
405 421 hook_name='PRE_CREATE_USER_HOOK',
406 422 kwargs_keys=(
407 423 'username', 'password', 'email', 'firstname', 'lastname', 'active',
408 424 'admin', 'created_by'))
409 425
410 426
411 427 create_pull_request = ExtensionCallback(
412 428 hook_name='CREATE_PULL_REQUEST',
413 429 kwargs_keys=(
414 430 'server_url', 'config', 'scm', 'username', 'ip', 'action',
415 431 'repository', 'pull_request_id', 'url', 'title', 'description',
416 432 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
417 433 'mergeable', 'source', 'target', 'author', 'reviewers'))
418 434
419 435
420 436 merge_pull_request = ExtensionCallback(
421 437 hook_name='MERGE_PULL_REQUEST',
422 438 kwargs_keys=(
423 439 'server_url', 'config', 'scm', 'username', 'ip', 'action',
424 440 'repository', 'pull_request_id', 'url', 'title', 'description',
425 441 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
426 442 'mergeable', 'source', 'target', 'author', 'reviewers'))
427 443
428 444
429 445 close_pull_request = ExtensionCallback(
430 446 hook_name='CLOSE_PULL_REQUEST',
431 447 kwargs_keys=(
432 448 'server_url', 'config', 'scm', 'username', 'ip', 'action',
433 449 'repository', 'pull_request_id', 'url', 'title', 'description',
434 450 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
435 451 'mergeable', 'source', 'target', 'author', 'reviewers'))
436 452
437 453
438 454 review_pull_request = ExtensionCallback(
439 455 hook_name='REVIEW_PULL_REQUEST',
440 456 kwargs_keys=(
441 457 'server_url', 'config', 'scm', 'username', 'ip', 'action',
442 458 'repository', 'pull_request_id', 'url', 'title', 'description',
443 459 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
444 460 'mergeable', 'source', 'target', 'author', 'reviewers'))
445 461
446 462
447 463 comment_pull_request = ExtensionCallback(
448 464 hook_name='COMMENT_PULL_REQUEST',
449 465 kwargs_keys=(
450 466 'server_url', 'config', 'scm', 'username', 'ip', 'action',
451 467 'repository', 'pull_request_id', 'url', 'title', 'description',
452 468 'status', 'comment', 'created_on', 'updated_on', 'commit_ids', 'review_status',
453 469 'mergeable', 'source', 'target', 'author', 'reviewers'))
454 470
455 471
456 472 comment_edit_pull_request = ExtensionCallback(
457 473 hook_name='COMMENT_EDIT_PULL_REQUEST',
458 474 kwargs_keys=(
459 475 'server_url', 'config', 'scm', 'username', 'ip', 'action',
460 476 'repository', 'pull_request_id', 'url', 'title', 'description',
461 477 'status', 'comment', 'created_on', 'updated_on', 'commit_ids', 'review_status',
462 478 'mergeable', 'source', 'target', 'author', 'reviewers'))
463 479
464 480
465 481 update_pull_request = ExtensionCallback(
466 482 hook_name='UPDATE_PULL_REQUEST',
467 483 kwargs_keys=(
468 484 'server_url', 'config', 'scm', 'username', 'ip', 'action',
469 485 'repository', 'pull_request_id', 'url', 'title', 'description',
470 486 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
471 487 'mergeable', 'source', 'target', 'author', 'reviewers'))
472 488
473 489
474 490 create_user = ExtensionCallback(
475 491 hook_name='CREATE_USER_HOOK',
476 492 kwargs_keys=(
477 493 'username', 'full_name_or_username', 'full_contact', 'user_id',
478 494 'name', 'firstname', 'short_contact', 'admin', 'lastname',
479 495 'ip_addresses', 'extern_type', 'extern_name',
480 496 'email', 'api_keys', 'last_login',
481 497 'full_name', 'active', 'password', 'emails',
482 498 'inherit_default_permissions', 'created_by', 'created_on'))
483 499
484 500
485 501 delete_user = ExtensionCallback(
486 502 hook_name='DELETE_USER_HOOK',
487 503 kwargs_keys=(
488 504 'username', 'full_name_or_username', 'full_contact', 'user_id',
489 505 'name', 'firstname', 'short_contact', 'admin', 'lastname',
490 506 'ip_addresses',
491 507 'email', 'last_login',
492 508 'full_name', 'active', 'password', 'emails',
493 509 'inherit_default_permissions', 'deleted_by'))
494 510
495 511
496 512 create_repository = ExtensionCallback(
497 513 hook_name='CREATE_REPO_HOOK',
498 514 kwargs_keys=(
499 515 'repo_name', 'repo_type', 'description', 'private', 'created_on',
500 516 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
501 517 'clone_uri', 'fork_id', 'group_id', 'created_by'))
502 518
503 519
504 520 delete_repository = ExtensionCallback(
505 521 hook_name='DELETE_REPO_HOOK',
506 522 kwargs_keys=(
507 523 'repo_name', 'repo_type', 'description', 'private', 'created_on',
508 524 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
509 525 'clone_uri', 'fork_id', 'group_id', 'deleted_by', 'deleted_on'))
510 526
511 527
512 528 comment_commit_repository = ExtensionCallback(
513 529 hook_name='COMMENT_COMMIT_REPO_HOOK',
514 530 kwargs_keys=(
515 531 'repo_name', 'repo_type', 'description', 'private', 'created_on',
516 532 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
517 533 'clone_uri', 'fork_id', 'group_id',
518 534 'repository', 'created_by', 'comment', 'commit'))
519 535
520 536 comment_edit_commit_repository = ExtensionCallback(
521 537 hook_name='COMMENT_EDIT_COMMIT_REPO_HOOK',
522 538 kwargs_keys=(
523 539 'repo_name', 'repo_type', 'description', 'private', 'created_on',
524 540 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
525 541 'clone_uri', 'fork_id', 'group_id',
526 542 'repository', 'created_by', 'comment', 'commit'))
527 543
528 544
529 545 create_repository_group = ExtensionCallback(
530 546 hook_name='CREATE_REPO_GROUP_HOOK',
531 547 kwargs_keys=(
532 548 'group_name', 'group_parent_id', 'group_description',
533 549 'group_id', 'user_id', 'created_by', 'created_on',
534 550 'enable_locking'))
@@ -1,96 +1,96 b''
1 1
2 2
3 3 # Copyright (C) 2016-2023 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import time
22 22 import logging
23 23
24 24 import rhodecode
25 25 from rhodecode.lib.auth import AuthUser
26 26 from rhodecode.lib.base import get_ip_addr, get_user_agent
27 27 from rhodecode.lib.middleware.utils import get_path_info
28 28 from rhodecode.lib.utils2 import safe_str, get_current_rhodecode_user
29 29
30 30
31 31 log = logging.getLogger(__name__)
32 32
33 33
34 34 class RequestWrapperTween(object):
35 35 def __init__(self, handler, registry):
36 36 self.handler = handler
37 37 self.registry = registry
38 38
39 39 # one-time configuration code goes here
40 40
41 41 def _get_user_info(self, request):
42 42 user = get_current_rhodecode_user(request)
43 43 if not user:
44 44 user = AuthUser.repr_user(ip=get_ip_addr(request.environ))
45 45 return user
46 46
47 47 def __call__(self, request):
48 48 start = time.time()
49 log.debug('Starting request time measurement')
49 log.debug('Starting request processing')
50 50 response = None
51 51 request.req_wrapper_start = start
52 52
53 53 try:
54 54 response = self.handler(request)
55 55 finally:
56 56 count = request.request_count()
57 57 _ver_ = rhodecode.__version__
58 58 _path = get_path_info(request.environ)
59 59 _auth_user = self._get_user_info(request)
60 60 ip = get_ip_addr(request.environ)
61 61 match_route = request.matched_route.name if request.matched_route else "NOT_FOUND"
62 62 resp_code = getattr(response, 'status_code', 'UNDEFINED')
63 63
64 64 total = time.time() - start
65 65 log.info(
66 'Req[%4s] %s %s Request to %s time: %.4fs [%s], RhodeCode %s',
66 'Finished request processing: req[%4s] %s %s Request to %s time: %.4fs [%s], RhodeCode %s',
67 67 count, _auth_user, request.environ.get('REQUEST_METHOD'),
68 68 _path, total, get_user_agent(request. environ), _ver_,
69 69 extra={"time": total, "ver": _ver_, "ip": ip,
70 70 "path": _path, "view_name": match_route, "code": resp_code}
71 71 )
72 72
73 73 statsd = request.registry.statsd
74 74 if statsd:
75 75 elapsed_time_ms = round(1000.0 * total) # use ms only
76 76 statsd.timing(
77 77 "rhodecode_req_timing.histogram", elapsed_time_ms,
78 78 tags=[
79 79 "view_name:{}".format(match_route),
80 80 "code:{}".format(resp_code)
81 81 ],
82 82 use_decimals=False
83 83 )
84 84 statsd.incr(
85 85 'rhodecode_req_total', tags=[
86 86 "view_name:{}".format(match_route),
87 87 "code:{}".format(resp_code)
88 88 ])
89 89
90 90 return response
91 91
92 92
93 93 def includeme(config):
94 94 config.add_tween(
95 95 'rhodecode.lib.middleware.request_wrapper.RequestWrapperTween',
96 96 )
@@ -1,161 +1,167 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 """
21 21 SimpleHG middleware for handling mercurial protocol request
22 22 (push/clone etc.). It's implemented with basic auth function
23 23 """
24 24
25 25 import logging
26 26 import urllib.parse
27 27 import urllib.request
28 28 import urllib.parse
29 29 import urllib.error
30 30
31 31 from rhodecode.lib import utils
32 32 from rhodecode.lib.ext_json import json
33 33 from rhodecode.lib.middleware import simplevcs
34 34 from rhodecode.lib.middleware.utils import get_path_info
35 35
36 36 log = logging.getLogger(__name__)
37 37
38 38
39 39 class SimpleHg(simplevcs.SimpleVCS):
40 40
41 41 SCM = 'hg'
42 42
43 43 def _get_repository_name(self, environ):
44 44 """
45 45 Gets repository name out of PATH_INFO header
46 46
47 47 :param environ: environ where PATH_INFO is stored
48 48 """
49 49 repo_name = get_path_info(environ)
50 50 if repo_name and repo_name.startswith('/'):
51 51 # remove only the first leading /
52 52 repo_name = repo_name[1:]
53 53 return repo_name.rstrip('/')
54 54
55 55 _ACTION_MAPPING = {
56 'between': 'pull',
57 'branches': 'pull',
58 'branchmap': 'pull',
59 'capabilities': 'pull',
56 60 'changegroup': 'pull',
57 61 'changegroupsubset': 'pull',
62 'changesetdata': 'pull',
63 'clonebundles': 'pull',
64 'clonebundles_manifest': 'pull',
65 'debugwireargs': 'pull',
66 'filedata': 'pull',
58 67 'getbundle': 'pull',
59 'stream_out': 'pull',
60 'listkeys': 'pull',
61 'between': 'pull',
62 'branchmap': 'pull',
63 'branches': 'pull',
64 'clonebundles': 'pull',
65 'capabilities': 'pull',
66 'debugwireargs': 'pull',
67 68 'heads': 'pull',
68 'lookup': 'pull',
69 69 'hello': 'pull',
70 70 'known': 'pull',
71 'listkeys': 'pull',
72 'lookup': 'pull',
73 'manifestdata': 'pull',
74 'narrow_widen': 'pull',
75 'protocaps': 'pull',
76 'stream_out': 'pull',
71 77
72 78 # largefiles
79 'getlfile': 'pull',
73 80 'putlfile': 'push',
74 'getlfile': 'pull',
75 81 'statlfile': 'pull',
76 82 'lheads': 'pull',
77 83
78 84 # evolve
79 85 'evoext_obshashrange_v1': 'pull',
80 86 'evoext_obshash': 'pull',
81 87 'evoext_obshash1': 'pull',
82 88
83 89 'unbundle': 'push',
84 90 'pushkey': 'push',
85 91 }
86 92
87 93 @classmethod
88 94 def _get_xarg_headers(cls, environ):
89 95 i = 1
90 96 chunks = [] # gather chunks stored in multiple 'hgarg_N'
91 97 while True:
92 98 head = environ.get('HTTP_X_HGARG_{}'.format(i))
93 99 if not head:
94 100 break
95 101 i += 1
96 102 chunks.append(urllib.parse.unquote_plus(head))
97 103 full_arg = ''.join(chunks)
98 104 pref = 'cmds='
99 105 if full_arg.startswith(pref):
100 106 # strip the cmds= header defining our batch commands
101 107 full_arg = full_arg[len(pref):]
102 108 cmds = full_arg.split(';')
103 109 return cmds
104 110
105 111 @classmethod
106 112 def _get_batch_cmd(cls, environ):
107 113 """
108 114 Handle batch command send commands. Those are ';' separated commands
109 115 sent by batch command that server needs to execute. We need to extract
110 116 those, and map them to our ACTION_MAPPING to get all push/pull commands
111 117 specified in the batch
112 118 """
113 119 default = 'push'
114 120 batch_cmds = []
115 121 try:
116 122 cmds = cls._get_xarg_headers(environ)
117 123 for pair in cmds:
118 124 parts = pair.split(' ', 1)
119 125 if len(parts) != 2:
120 126 continue
121 127 # entry should be in a format `key ARGS`
122 128 cmd, args = parts
123 129 action = cls._ACTION_MAPPING.get(cmd, default)
124 130 batch_cmds.append(action)
125 131 except Exception:
126 132 log.exception('Failed to extract batch commands operations')
127 133
128 134 # in case we failed, (e.g malformed data) assume it's PUSH sub-command
129 135 # for safety
130 136 return batch_cmds or [default]
131 137
132 138 def _get_action(self, environ):
133 139 """
134 140 Maps mercurial request commands into a pull or push command.
135 141 In case of unknown/unexpected data, it returns 'push' to be safe.
136 142
137 143 :param environ:
138 144 """
139 145 default = 'push'
140 146 query = urllib.parse.parse_qs(environ['QUERY_STRING'], keep_blank_values=True)
141 147
142 148 if 'cmd' in query:
143 149 cmd = query['cmd'][0]
144 150 if cmd == 'batch':
145 151 cmds = self._get_batch_cmd(environ)
146 152 if 'push' in cmds:
147 153 return 'push'
148 154 else:
149 155 return 'pull'
150 156 return self._ACTION_MAPPING.get(cmd, default)
151 157
152 158 return default
153 159
154 160 def _create_wsgi_app(self, repo_path, repo_name, config):
155 161 return self.scm_app.create_hg_wsgi_app(repo_path, repo_name, config)
156 162
157 163 def _create_config(self, extras, repo_name, scheme='http'):
158 164 config = utils.make_db_config(repo=repo_name)
159 165 config.set('rhodecode', 'RC_SCM_DATA', json.dumps(extras))
160 166
161 167 return config.serialize()
@@ -1,683 +1,662 b''
1 1
2 2
3 3 # Copyright (C) 2014-2023 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 SimpleVCS middleware for handling protocol request (push/clone etc.)
23 23 It's implemented with basic auth function
24 24 """
25 25
26 26 import os
27 27 import re
28 28 import logging
29 29 import importlib
30 30 from functools import wraps
31 31
32 32 import time
33 33 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
34 34
35 35 from pyramid.httpexceptions import (
36 36 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
37 37 from zope.cachedescriptors.property import Lazy as LazyProperty
38 38
39 39 import rhodecode
40 40 from rhodecode.authentication.base import authenticate, VCS_TYPE, loadplugin
41 41 from rhodecode.lib import rc_cache
42 42 from rhodecode.lib.svn_txn_utils import store_txn_id_data
43 43 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
44 44 from rhodecode.lib.base import (
45 45 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
46 46 from rhodecode.lib.exceptions import (UserCreationError, NotAllowedToCreateUserError)
47 47 from rhodecode.lib.hook_daemon.base import prepare_callback_daemon
48 48 from rhodecode.lib.middleware import appenlight
49 49 from rhodecode.lib.middleware.utils import scm_app_http
50 50 from rhodecode.lib.str_utils import safe_bytes, safe_int
51 51 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
52 52 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool
53 53 from rhodecode.lib.vcs.conf import settings as vcs_settings
54 54 from rhodecode.lib.vcs.backends import base
55 55
56 56 from rhodecode.model import meta
57 57 from rhodecode.model.db import User, Repository, PullRequest
58 58 from rhodecode.model.scm import ScmModel
59 59 from rhodecode.model.pull_request import PullRequestModel
60 60 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
61 61
62 62 log = logging.getLogger(__name__)
63 63
64 64
65 65 def initialize_generator(factory):
66 66 """
67 67 Initializes the returned generator by draining its first element.
68 68
69 69 This can be used to give a generator an initializer, which is the code
70 70 up to the first yield statement. This decorator enforces that the first
71 71 produced element has the value ``"__init__"`` to make its special
72 72 purpose very explicit in the using code.
73 73 """
74 74
75 75 @wraps(factory)
76 76 def wrapper(*args, **kwargs):
77 77 gen = factory(*args, **kwargs)
78 78 try:
79 79 init = next(gen)
80 80 except StopIteration:
81 81 raise ValueError('Generator must yield at least one element.')
82 82 if init != "__init__":
83 83 raise ValueError('First yielded element must be "__init__".')
84 84 return gen
85 85 return wrapper
86 86
87 87
88 88 class SimpleVCS(object):
89 89 """Common functionality for SCM HTTP handlers."""
90 90
91 91 SCM = 'unknown'
92 92
93 93 acl_repo_name = None
94 94 url_repo_name = None
95 95 vcs_repo_name = None
96 96 rc_extras = {}
97 97
98 98 # We have to handle requests to shadow repositories different than requests
99 99 # to normal repositories. Therefore we have to distinguish them. To do this
100 100 # we use this regex which will match only on URLs pointing to shadow
101 101 # repositories.
102 102 shadow_repo_re = re.compile(
103 103 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
104 104 '(?P<target>{slug_pat})/' # target repo
105 105 'pull-request/(?P<pr_id>\\d+)/' # pull request
106 106 'repository$' # shadow repo
107 107 .format(slug_pat=SLUG_RE.pattern))
108 108
109 109 def __init__(self, config, registry):
110 110 self.registry = registry
111 111 self.config = config
112 112 # re-populated by specialized middleware
113 113 self.repo_vcs_config = base.Config()
114 114
115 115 rc_settings = SettingsModel().get_all_settings(cache=True, from_request=False)
116 116 realm = rc_settings.get('rhodecode_realm') or 'RhodeCode AUTH'
117 117
118 118 # authenticate this VCS request using authfunc
119 119 auth_ret_code_detection = \
120 120 str2bool(self.config.get('auth_ret_code_detection', False))
121 121 self.authenticate = BasicAuth(
122 122 '', authenticate, registry, config.get('auth_ret_code'),
123 123 auth_ret_code_detection, rc_realm=realm)
124 124 self.ip_addr = '0.0.0.0'
125 125
126 126 @LazyProperty
127 127 def global_vcs_config(self):
128 128 try:
129 129 return VcsSettingsModel().get_ui_settings_as_config_obj()
130 130 except Exception:
131 131 return base.Config()
132 132
133 133 @property
134 134 def base_path(self):
135 135 settings_path = self.config.get('repo_store.path')
136 136
137 137 if not settings_path:
138 138 raise ValueError('FATAL: repo_store.path is empty')
139 139 return settings_path
140 140
141 141 def set_repo_names(self, environ):
142 142 """
143 143 This will populate the attributes acl_repo_name, url_repo_name,
144 144 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
145 145 shadow) repositories all names are equal. In case of requests to a
146 146 shadow repository the acl-name points to the target repo of the pull
147 147 request and the vcs-name points to the shadow repo file system path.
148 148 The url-name is always the URL used by the vcs client program.
149 149
150 150 Example in case of a shadow repo:
151 151 acl_repo_name = RepoGroup/MyRepo
152 152 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
153 153 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
154 154 """
155 155 # First we set the repo name from URL for all attributes. This is the
156 156 # default if handling normal (non shadow) repo requests.
157 157 self.url_repo_name = self._get_repository_name(environ)
158 158 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
159 159 self.is_shadow_repo = False
160 160
161 161 # Check if this is a request to a shadow repository.
162 162 match = self.shadow_repo_re.match(self.url_repo_name)
163 163 if match:
164 164 match_dict = match.groupdict()
165 165
166 166 # Build acl repo name from regex match.
167 167 acl_repo_name = safe_str('{groups}{target}'.format(
168 168 groups=match_dict['groups'] or '',
169 169 target=match_dict['target']))
170 170
171 171 # Retrieve pull request instance by ID from regex match.
172 172 pull_request = PullRequest.get(match_dict['pr_id'])
173 173
174 174 # Only proceed if we got a pull request and if acl repo name from
175 175 # URL equals the target repo name of the pull request.
176 176 if pull_request and (acl_repo_name == pull_request.target_repo.repo_name):
177 177
178 178 # Get file system path to shadow repository.
179 179 workspace_id = PullRequestModel()._workspace_id(pull_request)
180 180 vcs_repo_name = pull_request.target_repo.get_shadow_repository_path(workspace_id)
181 181
182 182 # Store names for later usage.
183 183 self.vcs_repo_name = vcs_repo_name
184 184 self.acl_repo_name = acl_repo_name
185 185 self.is_shadow_repo = True
186 186
187 187 log.debug('Setting all VCS repository names: %s', {
188 188 'acl_repo_name': self.acl_repo_name,
189 189 'url_repo_name': self.url_repo_name,
190 190 'vcs_repo_name': self.vcs_repo_name,
191 191 })
192 192
193 193 @property
194 194 def scm_app(self):
195 195 custom_implementation = self.config['vcs.scm_app_implementation']
196 196 if custom_implementation == 'http':
197 197 log.debug('Using HTTP implementation of scm app.')
198 198 scm_app_impl = scm_app_http
199 199 else:
200 200 log.debug('Using custom implementation of scm_app: "{}"'.format(
201 201 custom_implementation))
202 202 scm_app_impl = importlib.import_module(custom_implementation)
203 203 return scm_app_impl
204 204
205 205 def _get_by_id(self, repo_name):
206 206 """
207 207 Gets a special pattern _<ID> from clone url and tries to replace it
208 208 with a repository_name for support of _<ID> non changeable urls
209 209 """
210 210
211 211 data = repo_name.split('/')
212 212 if len(data) >= 2:
213 213 from rhodecode.model.repo import RepoModel
214 214 by_id_match = RepoModel().get_repo_by_id(repo_name)
215 215 if by_id_match:
216 216 data[1] = by_id_match.repo_name
217 217
218 218 # Because PEP-3333-WSGI uses bytes-tunneled-in-latin-1 as PATH_INFO
219 219 # and we use this data
220 220 maybe_new_path = '/'.join(data)
221 221 return safe_bytes(maybe_new_path).decode('latin1')
222 222
223 223 def _invalidate_cache(self, repo_name):
224 224 """
225 225 Set's cache for this repository for invalidation on next access
226 226
227 227 :param repo_name: full repo name, also a cache key
228 228 """
229 229 ScmModel().mark_for_invalidation(repo_name)
230 230
231 231 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
232 232 db_repo = Repository.get_by_repo_name(repo_name)
233 233 if not db_repo:
234 234 log.debug('Repository `%s` not found inside the database.',
235 235 repo_name)
236 236 return False
237 237
238 238 if db_repo.repo_type != scm_type:
239 239 log.warning(
240 240 'Repository `%s` have incorrect scm_type, expected %s got %s',
241 241 repo_name, db_repo.repo_type, scm_type)
242 242 return False
243 243
244 244 config = db_repo._config
245 245 config.set('extensions', 'largefiles', '')
246 246 return is_valid_repo(
247 247 repo_name, base_path,
248 248 explicit_scm=scm_type, expect_scm=scm_type, config=config)
249 249
250 250 def valid_and_active_user(self, user):
251 251 """
252 252 Checks if that user is not empty, and if it's actually object it checks
253 253 if he's active.
254 254
255 255 :param user: user object or None
256 256 :return: boolean
257 257 """
258 258 if user is None:
259 259 return False
260 260
261 261 elif user.active:
262 262 return True
263 263
264 264 return False
265 265
266 266 @property
267 267 def is_shadow_repo_dir(self):
268 268 return os.path.isdir(self.vcs_repo_name)
269 269
270 270 def _check_permission(self, action, user, auth_user, repo_name, ip_addr=None,
271 271 plugin_id='', plugin_cache_active=False, cache_ttl=0):
272 272 """
273 273 Checks permissions using action (push/pull) user and repository
274 274 name. If plugin_cache and ttl is set it will use the plugin which
275 275 authenticated the user to store the cached permissions result for N
276 276 amount of seconds as in cache_ttl
277 277
278 278 :param action: push or pull action
279 279 :param user: user instance
280 280 :param repo_name: repository name
281 281 """
282 282
283 283 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
284 284 plugin_id, plugin_cache_active, cache_ttl)
285 285
286 286 user_id = user.user_id
287 287 cache_namespace_uid = f'cache_user_auth.{rc_cache.PERMISSIONS_CACHE_VER}.{user_id}'
288 288 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
289 289
290 290 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
291 291 expiration_time=cache_ttl,
292 292 condition=plugin_cache_active)
293 293 def compute_perm_vcs(
294 294 cache_name, plugin_id, action, user_id, repo_name, ip_addr):
295 295
296 log.debug('auth: calculating permission access now...')
296 log.debug('auth: calculating permission access now for vcs operation: %s', action)
297 297 # check IP
298 298 inherit = user.inherit_default_permissions
299 299 ip_allowed = AuthUser.check_ip_allowed(
300 300 user_id, ip_addr, inherit_from_default=inherit)
301 301 if ip_allowed:
302 302 log.info('Access for IP:%s allowed', ip_addr)
303 303 else:
304 304 return False
305 305
306 306 if action == 'push':
307 307 perms = ('repository.write', 'repository.admin')
308 308 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
309 309 return False
310 310
311 311 else:
312 312 # any other action need at least read permission
313 313 perms = (
314 314 'repository.read', 'repository.write', 'repository.admin')
315 315 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
316 316 return False
317 317
318 318 return True
319 319
320 320 start = time.time()
321 321 log.debug('Running plugin `%s` permissions check', plugin_id)
322 322
323 323 # for environ based auth, password can be empty, but then the validation is
324 324 # on the server that fills in the env data needed for authentication
325 325 perm_result = compute_perm_vcs(
326 326 'vcs_permissions', plugin_id, action, user.user_id, repo_name, ip_addr)
327 327
328 328 auth_time = time.time() - start
329 329 log.debug('Permissions for plugin `%s` completed in %.4fs, '
330 330 'expiration time of fetched cache %.1fs.',
331 331 plugin_id, auth_time, cache_ttl)
332 332
333 333 return perm_result
334 334
335 335 def _get_http_scheme(self, environ):
336 336 try:
337 337 return environ['wsgi.url_scheme']
338 338 except Exception:
339 339 log.exception('Failed to read http scheme')
340 340 return 'http'
341 341
342 def _check_ssl(self, environ, start_response):
343 """
344 Checks the SSL check flag and returns False if SSL is not present
345 and required True otherwise
346 """
347 org_proto = environ['wsgi._org_proto']
348 # check if we have SSL required ! if not it's a bad request !
349 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
350 if require_ssl and org_proto == 'http':
351 log.debug(
352 'Bad request: detected protocol is `%s` and '
353 'SSL/HTTPS is required.', org_proto)
354 return False
355 return True
356
357 342 def _get_default_cache_ttl(self):
358 343 # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
359 344 plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode')
360 345 plugin_settings = plugin.get_settings()
361 346 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(
362 347 plugin_settings) or (False, 0)
363 348 return plugin_cache_active, cache_ttl
364 349
365 350 def __call__(self, environ, start_response):
366 351 try:
367 352 return self._handle_request(environ, start_response)
368 353 except Exception:
369 354 log.exception("Exception while handling request")
370 355 appenlight.track_exception(environ)
371 356 return HTTPInternalServerError()(environ, start_response)
372 357 finally:
373 358 meta.Session.remove()
374 359
375 360 def _handle_request(self, environ, start_response):
376 if not self._check_ssl(environ, start_response):
377 reason = ('SSL required, while RhodeCode was unable '
378 'to detect this as SSL request')
379 log.debug('User not allowed to proceed, %s', reason)
380 return HTTPNotAcceptable(reason)(environ, start_response)
381
382 361 if not self.url_repo_name:
383 362 log.warning('Repository name is empty: %s', self.url_repo_name)
384 363 # failed to get repo name, we fail now
385 364 return HTTPNotFound()(environ, start_response)
386 365 log.debug('Extracted repo name is %s', self.url_repo_name)
387 366
388 367 ip_addr = get_ip_addr(environ)
389 368 user_agent = get_user_agent(environ)
390 369 username = None
391 370
392 371 # skip passing error to error controller
393 372 environ['pylons.status_code_redirect'] = True
394 373
395 374 # ======================================================================
396 375 # GET ACTION PULL or PUSH
397 376 # ======================================================================
398 377 action = self._get_action(environ)
399 378
400 379 # ======================================================================
401 380 # Check if this is a request to a shadow repository of a pull request.
402 381 # In this case only pull action is allowed.
403 382 # ======================================================================
404 383 if self.is_shadow_repo and action != 'pull':
405 384 reason = 'Only pull action is allowed for shadow repositories.'
406 385 log.debug('User not allowed to proceed, %s', reason)
407 386 return HTTPNotAcceptable(reason)(environ, start_response)
408 387
409 388 # Check if the shadow repo actually exists, in case someone refers
410 389 # to it, and it has been deleted because of successful merge.
411 390 if self.is_shadow_repo and not self.is_shadow_repo_dir:
412 391 log.debug(
413 392 'Shadow repo detected, and shadow repo dir `%s` is missing',
414 393 self.is_shadow_repo_dir)
415 394 return HTTPNotFound()(environ, start_response)
416 395
417 396 # ======================================================================
418 397 # CHECK ANONYMOUS PERMISSION
419 398 # ======================================================================
420 399 detect_force_push = False
421 400 check_branch_perms = False
422 401 if action in ['pull', 'push']:
423 402 user_obj = anonymous_user = User.get_default_user()
424 403 auth_user = user_obj.AuthUser()
425 404 username = anonymous_user.username
426 405 if anonymous_user.active:
427 406 plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
428 407 # ONLY check permissions if the user is activated
429 408 anonymous_perm = self._check_permission(
430 409 action, anonymous_user, auth_user, self.acl_repo_name, ip_addr,
431 410 plugin_id='anonymous_access',
432 411 plugin_cache_active=plugin_cache_active,
433 412 cache_ttl=cache_ttl,
434 413 )
435 414 else:
436 415 anonymous_perm = False
437 416
438 417 if not anonymous_user.active or not anonymous_perm:
439 418 if not anonymous_user.active:
440 419 log.debug('Anonymous access is disabled, running '
441 420 'authentication')
442 421
443 422 if not anonymous_perm:
444 423 log.debug('Not enough credentials to access repo: `%s` '
445 424 'repository as anonymous user', self.acl_repo_name)
446 425
447 426 username = None
448 427 # ==============================================================
449 428 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
450 429 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
451 430 # ==============================================================
452 431
453 432 # try to auth based on environ, container auth methods
454 433 log.debug('Running PRE-AUTH for container|headers based authentication')
455 434
456 435 # headers auth, by just reading special headers and bypass the auth with user/passwd
457 436 pre_auth = authenticate(
458 437 '', '', environ, VCS_TYPE, registry=self.registry,
459 438 acl_repo_name=self.acl_repo_name)
460 439
461 440 if pre_auth and pre_auth.get('username'):
462 441 username = pre_auth['username']
463 442 log.debug('PRE-AUTH got `%s` as username', username)
464 443 if pre_auth:
465 444 log.debug('PRE-AUTH successful from %s',
466 445 pre_auth.get('auth_data', {}).get('_plugin'))
467 446
468 447 # If not authenticated by the container, running basic auth
469 448 # before inject the calling repo_name for special scope checks
470 449 self.authenticate.acl_repo_name = self.acl_repo_name
471 450
472 451 plugin_cache_active, cache_ttl = False, 0
473 452 plugin = None
474 453
475 454 # regular auth chain
476 455 if not username:
477 456 self.authenticate.realm = self.authenticate.get_rc_realm()
478 457
479 458 try:
480 459 auth_result = self.authenticate(environ)
481 460 except (UserCreationError, NotAllowedToCreateUserError) as e:
482 461 log.error(e)
483 462 reason = safe_str(e)
484 463 return HTTPNotAcceptable(reason)(environ, start_response)
485 464
486 465 if isinstance(auth_result, dict):
487 466 AUTH_TYPE.update(environ, 'basic')
488 467 REMOTE_USER.update(environ, auth_result['username'])
489 468 username = auth_result['username']
490 469 plugin = auth_result.get('auth_data', {}).get('_plugin')
491 470 log.info(
492 471 'MAIN-AUTH successful for user `%s` from %s plugin',
493 472 username, plugin)
494 473
495 474 plugin_cache_active, cache_ttl = auth_result.get(
496 475 'auth_data', {}).get('_ttl_cache') or (False, 0)
497 476 else:
498 477 return auth_result.wsgi_application(environ, start_response)
499 478
500 479 # ==============================================================
501 480 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
502 481 # ==============================================================
503 482 user = User.get_by_username(username)
504 483 if not self.valid_and_active_user(user):
505 484 return HTTPForbidden()(environ, start_response)
506 485 username = user.username
507 486 user_id = user.user_id
508 487
509 488 # check user attributes for password change flag
510 489 user_obj = user
511 490 auth_user = user_obj.AuthUser()
512 491 if user_obj and user_obj.username != User.DEFAULT_USER and \
513 492 user_obj.user_data.get('force_password_change'):
514 493 reason = 'password change required'
515 494 log.debug('User not allowed to authenticate, %s', reason)
516 495 return HTTPNotAcceptable(reason)(environ, start_response)
517 496
518 497 # check permissions for this repository
519 498 perm = self._check_permission(
520 499 action, user, auth_user, self.acl_repo_name, ip_addr,
521 500 plugin, plugin_cache_active, cache_ttl)
522 501 if not perm:
523 502 return HTTPForbidden()(environ, start_response)
524 503 environ['rc_auth_user_id'] = str(user_id)
525 504
526 505 if action == 'push':
527 506 perms = auth_user.get_branch_permissions(self.acl_repo_name)
528 507 if perms:
529 508 check_branch_perms = True
530 509 detect_force_push = True
531 510
532 511 # extras are injected into UI object and later available
533 512 # in hooks executed by RhodeCode
534 513 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
535 514
536 515 extras = vcs_operation_context(
537 516 environ, repo_name=self.acl_repo_name, username=username,
538 517 action=action, scm=self.SCM, check_locking=check_locking,
539 518 is_shadow_repo=self.is_shadow_repo, check_branch_perms=check_branch_perms,
540 519 detect_force_push=detect_force_push
541 520 )
542 521
543 522 # ======================================================================
544 523 # REQUEST HANDLING
545 524 # ======================================================================
546 525 repo_path = os.path.join(
547 526 safe_str(self.base_path), safe_str(self.vcs_repo_name))
548 527 log.debug('Repository path is %s', repo_path)
549 528
550 529 fix_PATH()
551 530
552 531 log.info(
553 532 '%s action on %s repo "%s" by "%s" from %s %s',
554 533 action, self.SCM, safe_str(self.url_repo_name),
555 534 safe_str(username), ip_addr, user_agent)
556 535
557 536 return self._generate_vcs_response(
558 537 environ, start_response, repo_path, extras, action)
559 538
560 539 def _get_txn_id(self, environ):
561 540
562 541 for k in ['RAW_URI', 'HTTP_DESTINATION']:
563 542 url = environ.get(k)
564 543 if not url:
565 544 continue
566 545
567 546 # regex to search for svn-txn-id
568 547 pattern = r'/!svn/txr/([^/]+)/'
569 548
570 549 # Search for the pattern in the URL
571 550 match = re.search(pattern, url)
572 551
573 552 # Check if a match is found and extract the captured group
574 553 if match:
575 554 txn_id = match.group(1)
576 555 return txn_id
577 556
578 557 @initialize_generator
579 558 def _generate_vcs_response(
580 559 self, environ, start_response, repo_path, extras, action):
581 560 """
582 561 Returns a generator for the response content.
583 562
584 563 This method is implemented as a generator, so that it can trigger
585 564 the cache validation after all content sent back to the client. It
586 565 also handles the locking exceptions which will be triggered when
587 566 the first chunk is produced by the underlying WSGI application.
588 567 """
589 568 svn_txn_id = ''
590 569 if action == 'push':
591 570 svn_txn_id = self._get_txn_id(environ)
592 571
593 572 callback_daemon, extras = self._prepare_callback_daemon(
594 573 extras, environ, action, txn_id=svn_txn_id)
595 574
596 575 if svn_txn_id:
597 576
598 577 port = safe_int(extras['hooks_uri'].split(':')[-1])
599 578 txn_id_data = extras.copy()
600 579 txn_id_data.update({'port': port})
601 580 txn_id_data.update({'req_method': environ['REQUEST_METHOD']})
602 581
603 582 full_repo_path = repo_path
604 583 store_txn_id_data(full_repo_path, svn_txn_id, txn_id_data)
605 584
606 585 log.debug('HOOKS extras is %s', extras)
607 586
608 587 http_scheme = self._get_http_scheme(environ)
609 588
610 589 config = self._create_config(extras, self.acl_repo_name, scheme=http_scheme)
611 590 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
612 591 with callback_daemon:
613 592 app.rc_extras = extras
614 593
615 594 try:
616 595 response = app(environ, start_response)
617 596 finally:
618 597 # This statement works together with the decorator
619 598 # "initialize_generator" above. The decorator ensures that
620 599 # we hit the first yield statement before the generator is
621 600 # returned back to the WSGI server. This is needed to
622 601 # ensure that the call to "app" above triggers the
623 602 # needed callback to "start_response" before the
624 603 # generator is actually used.
625 604 yield "__init__"
626 605
627 606 # iter content
628 607 for chunk in response:
629 608 yield chunk
630 609
631 610 try:
632 611 # invalidate cache on push
633 612 if action == 'push':
634 613 self._invalidate_cache(self.url_repo_name)
635 614 finally:
636 615 meta.Session.remove()
637 616
638 617 def _get_repository_name(self, environ):
639 618 """Get repository name out of the environmnent
640 619
641 620 :param environ: WSGI environment
642 621 """
643 622 raise NotImplementedError()
644 623
645 624 def _get_action(self, environ):
646 625 """Map request commands into a pull or push command.
647 626
648 627 :param environ: WSGI environment
649 628 """
650 629 raise NotImplementedError()
651 630
652 631 def _create_wsgi_app(self, repo_path, repo_name, config):
653 632 """Return the WSGI app that will finally handle the request."""
654 633 raise NotImplementedError()
655 634
656 635 def _create_config(self, extras, repo_name, scheme='http'):
657 636 """Create a safe config representation."""
658 637 raise NotImplementedError()
659 638
660 639 def _should_use_callback_daemon(self, extras, environ, action):
661 640 if extras.get('is_shadow_repo'):
662 641 # we don't want to execute hooks, and callback daemon for shadow repos
663 642 return False
664 643 return True
665 644
666 645 def _prepare_callback_daemon(self, extras, environ, action, txn_id=None):
667 646 protocol = vcs_settings.HOOKS_PROTOCOL
668 647
669 648 if not self._should_use_callback_daemon(extras, environ, action):
670 649 # disable callback daemon for actions that don't require it
671 650 protocol = 'local'
672 651
673 652 return prepare_callback_daemon(
674 653 extras, protocol=protocol,
675 654 host=vcs_settings.HOOKS_HOST, txn_id=txn_id)
676 655
677 656
678 657 def _should_check_locking(query_string):
679 658 # this is kind of hacky, but due to how mercurial handles client-server
680 659 # server see all operation on commit; bookmarks, phases and
681 660 # obsolescence marker in different transaction, we don't want to check
682 661 # locking on those
683 662 return query_string not in ['cmd=listkeys']
@@ -1,308 +1,312 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import gzip
21 21 import shutil
22 22 import logging
23 23 import tempfile
24 24 import urllib.parse
25 25
26 26 from webob.exc import HTTPNotFound
27 27
28 28 import rhodecode
29 29 from rhodecode.apps._base import ADMIN_PREFIX
30 30 from rhodecode.lib.middleware.utils import get_path_info
31 31 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
32 32 from rhodecode.lib.middleware.simplegit import SimpleGit, GIT_PROTO_PAT
33 33 from rhodecode.lib.middleware.simplehg import SimpleHg
34 34 from rhodecode.lib.middleware.simplesvn import SimpleSvn
35 35 from rhodecode.lib.str_utils import safe_str
36 36 from rhodecode.model.settings import VcsSettingsModel
37 37
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41 VCS_TYPE_KEY = '_rc_vcs_type'
42 42 VCS_TYPE_SKIP = '_rc_vcs_skip'
43 43
44 44
45 45 def is_git(environ):
46 46 """
47 47 Returns True if requests should be handled by GIT wsgi middleware
48 48 """
49 49 path_info = get_path_info(environ)
50 50 is_git_path = GIT_PROTO_PAT.match(path_info)
51 51 log.debug(
52 52 'request path: `%s` detected as GIT PROTOCOL %s', path_info,
53 53 is_git_path is not None)
54 54
55 55 return is_git_path
56 56
57 57
58 58 def is_hg(environ):
59 59 """
60 60 Returns True if requests target is mercurial server - header
61 61 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
62 62 """
63 63 is_hg_path = False
64 64
65 65 http_accept = environ.get('HTTP_ACCEPT')
66 66
67 67 if http_accept and http_accept.startswith('application/mercurial'):
68 68 query = urllib.parse.parse_qs(environ['QUERY_STRING'])
69 69 if 'cmd' in query:
70 70 is_hg_path = True
71 71
72 72 path_info = get_path_info(environ)
73 73 log.debug(
74 74 'request path: `%s` detected as HG PROTOCOL %s', path_info,
75 75 is_hg_path)
76 76
77 77 return is_hg_path
78 78
79 79
80 80 def is_svn(environ):
81 81 """
82 82 Returns True if requests target is Subversion server
83 83 """
84 84
85 85 http_dav = environ.get('HTTP_DAV', '')
86 86 magic_path_segment = rhodecode.CONFIG.get(
87 87 'rhodecode_subversion_magic_path', '/!svn')
88 88 path_info = get_path_info(environ)
89 89 req_method = environ['REQUEST_METHOD']
90 90
91 91 is_svn_path = (
92 92 'subversion' in http_dav or
93 93 magic_path_segment in path_info
94 94 or req_method in ['PROPFIND', 'PROPPATCH', 'HEAD']
95 95 )
96 96 log.debug(
97 97 'request path: `%s` detected as SVN PROTOCOL %s', path_info,
98 98 is_svn_path)
99 99
100 100 return is_svn_path
101 101
102 102
103 103 class GunzipMiddleware(object):
104 104 """
105 105 WSGI middleware that unzips gzip-encoded requests before
106 106 passing on to the underlying application.
107 107 """
108 108
109 109 def __init__(self, application):
110 110 self.app = application
111 111
112 112 def __call__(self, environ, start_response):
113 113 accepts_encoding_header = safe_str(environ.get('HTTP_CONTENT_ENCODING', ''))
114 114
115 115 if 'gzip' in accepts_encoding_header:
116 116 log.debug('gzip detected, now running gunzip wrapper')
117 117 wsgi_input = environ['wsgi.input']
118 118
119 119 if not hasattr(environ['wsgi.input'], 'seek'):
120 120 # The gzip implementation in the standard library of Python 2.x
121 121 # requires the '.seek()' and '.tell()' methods to be available
122 122 # on the input stream. Read the data into a temporary file to
123 123 # work around this limitation.
124 124
125 125 wsgi_input = tempfile.SpooledTemporaryFile(64 * 1024 * 1024)
126 126 shutil.copyfileobj(environ['wsgi.input'], wsgi_input)
127 127 wsgi_input.seek(0)
128 128
129 129 environ['wsgi.input'] = gzip.GzipFile(fileobj=wsgi_input, mode='r')
130 130 # since we "Ungzipped" the content we say now it's no longer gzip
131 131 # content encoding
132 132 del environ['HTTP_CONTENT_ENCODING']
133 133
134 134 # content length has changes ? or i'm not sure
135 135 if 'CONTENT_LENGTH' in environ:
136 136 del environ['CONTENT_LENGTH']
137 137 else:
138 138 log.debug('content not gzipped, gzipMiddleware passing '
139 139 'request further')
140 140 return self.app(environ, start_response)
141 141
142 142
143 143 def is_vcs_call(environ):
144 144 if VCS_TYPE_KEY in environ:
145 145 raw_type = environ[VCS_TYPE_KEY]
146 146 return raw_type and raw_type != VCS_TYPE_SKIP
147 147 return False
148 148
149 149
150 150 def detect_vcs_request(environ, backends):
151 151 checks = {
152 152 'hg': (is_hg, SimpleHg),
153 153 'git': (is_git, SimpleGit),
154 154 'svn': (is_svn, SimpleSvn),
155 155 }
156 156 handler = None
157 157 # List of path views first chunk we don't do any checks
158 158 white_list = [
159 159 # favicon often requested by browsers
160 160 'favicon.ico',
161 161
162 # static files no detection
163 '_static++',
164
165 # debug-toolbar
166 '_debug_toolbar++',
167
162 168 # e.g /_file_store/download
163 169 '_file_store++',
164 170
165 171 # login
166 "_admin/login",
172 f"{ADMIN_PREFIX}/login",
173 f"{ADMIN_PREFIX}/logout",
167 174
168 175 # 2fa
169 176 f"{ADMIN_PREFIX}/check_2fa",
170 177 f"{ADMIN_PREFIX}/setup_2fa",
171 178
172 179 # _admin/api is safe too
173 180 f'{ADMIN_PREFIX}/api',
174 181
175 182 # _admin/gist is safe too
176 183 f'{ADMIN_PREFIX}/gists++',
177 184
178 185 # _admin/my_account is safe too
179 186 f'{ADMIN_PREFIX}/my_account++',
180 187
181 # static files no detection
182 '_static++',
183
184 # debug-toolbar
185 '_debug_toolbar++',
186
187 188 # skip ops ping, status
188 189 f'{ADMIN_PREFIX}/ops/ping',
189 190 f'{ADMIN_PREFIX}/ops/status',
190 191
191 192 # full channelstream connect should be VCS skipped
192 193 f'{ADMIN_PREFIX}/channelstream/connect',
193 194
194 195 '++/repo_creating_check'
195 196 ]
197
196 198 path_info = get_path_info(environ)
197 199 path_url = path_info.lstrip('/')
198 200 req_method = environ.get('REQUEST_METHOD')
199 201
200 202 for item in white_list:
203 item = item.lstrip('/')
204
201 205 if item.endswith('++') and path_url.startswith(item[:-2]):
202 206 log.debug('path `%s` in whitelist (match:%s), skipping...', path_url, item)
203 207 return handler
204 208 if item.startswith('++') and path_url.endswith(item[2:]):
205 209 log.debug('path `%s` in whitelist (match:%s), skipping...', path_url, item)
206 210 return handler
207 211 if item == path_url:
208 212 log.debug('path `%s` in whitelist (match:%s), skipping...', path_url, item)
209 213 return handler
210 214
211 215 if VCS_TYPE_KEY in environ:
212 216 raw_type = environ[VCS_TYPE_KEY]
213 217 if raw_type == VCS_TYPE_SKIP:
214 218 log.debug('got `skip` marker for vcs detection, skipping...')
215 219 return handler
216 220
217 221 _check, handler = checks.get(raw_type) or [None, None]
218 222 if handler:
219 223 log.debug('got handler:%s from environ', handler)
220 224
221 225 if not handler:
222 226 log.debug('request start: checking if request for `%s:%s` is of VCS type in order: %s',
223 227 req_method, path_url, backends)
224 228 for vcs_type in backends:
225 229 vcs_check, _handler = checks[vcs_type]
226 230 if vcs_check(environ):
227 231 log.debug('vcs handler found %s', _handler)
228 232 handler = _handler
229 233 break
230 234
231 235 return handler
232 236
233 237
234 238 class VCSMiddleware(object):
235 239
236 240 def __init__(self, app, registry, config, appenlight_client):
237 241 self.application = app
238 242 self.registry = registry
239 243 self.config = config
240 244 self.appenlight_client = appenlight_client
241 245 self.use_gzip = True
242 246 # order in which we check the middlewares, based on vcs.backends config
243 247 self.check_middlewares = config['vcs.backends']
244 248
245 249 def vcs_config(self, repo_name=None):
246 250 """
247 251 returns serialized VcsSettings
248 252 """
249 253 try:
250 254 return VcsSettingsModel(
251 255 repo=repo_name).get_ui_settings_as_config_obj()
252 256 except Exception:
253 257 pass
254 258
255 259 def wrap_in_gzip_if_enabled(self, app, config):
256 260 if self.use_gzip:
257 261 app = GunzipMiddleware(app)
258 262 return app
259 263
260 264 def _get_handler_app(self, environ):
261 265 app = None
262 266 log.debug('VCSMiddleware: detecting vcs type.')
263 267 handler = detect_vcs_request(environ, self.check_middlewares)
264 268 if handler:
265 269 app = handler(self.config, self.registry)
266 270
267 271 return app
268 272
269 273 def __call__(self, environ, start_response):
270 274 # check if we handle one of interesting protocols, optionally extract
271 275 # specific vcsSettings and allow changes of how things are wrapped
272 276 vcs_handler = self._get_handler_app(environ)
273 277 if vcs_handler:
274 278 # translate the _REPO_ID into real repo NAME for usage
275 279 # in middleware
276 280
277 281 path_info = get_path_info(environ)
278 282 environ['PATH_INFO'] = vcs_handler._get_by_id(path_info)
279 283
280 284 # Set acl, url and vcs repo names.
281 285 vcs_handler.set_repo_names(environ)
282 286
283 287 # register repo config back to the handler
284 288 vcs_conf = self.vcs_config(vcs_handler.acl_repo_name)
285 289 # maybe damaged/non existent settings. We still want to
286 290 # pass that point to validate on is_valid_and_existing_repo
287 291 # and return proper HTTP Code back to client
288 292 if vcs_conf:
289 293 vcs_handler.repo_vcs_config = vcs_conf
290 294
291 295 # check for type, presence in database and on filesystem
292 296 if not vcs_handler.is_valid_and_existing_repo(
293 297 vcs_handler.acl_repo_name,
294 298 vcs_handler.base_path,
295 299 vcs_handler.SCM):
296 300 return HTTPNotFound()(environ, start_response)
297 301
298 302 environ['REPO_NAME'] = vcs_handler.url_repo_name
299 303
300 304 # Wrap handler in middlewares if they are enabled.
301 305 vcs_handler = self.wrap_in_gzip_if_enabled(
302 306 vcs_handler, self.config)
303 307 vcs_handler, _ = wrap_in_appenlight_if_enabled(
304 308 vcs_handler, self.config, self.appenlight_client)
305 309
306 310 return vcs_handler(environ, start_response)
307 311
308 312 return self.application(environ, start_response)
@@ -1,335 +1,342 b''
1 1 # Copyright (C) 2015-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import errno
20 20 import fcntl
21 21 import functools
22 22 import logging
23 23 import os
24 24 import pickle
25 25 import time
26 26
27 27 import gevent
28 28 import msgpack
29 29 import redis
30 30
31 31 flock_org = fcntl.flock
32 32 from typing import Union
33 33
34 34 from dogpile.cache.api import Deserializer, Serializer
35 35 from dogpile.cache.backends import file as file_backend
36 36 from dogpile.cache.backends import memory as memory_backend
37 37 from dogpile.cache.backends import redis as redis_backend
38 38 from dogpile.cache.backends.file import FileLock
39 39 from dogpile.cache.util import memoized_property
40 40
41 from rhodecode.lib.memory_lru_dict import LRUDict, LRUDictDebug
42 from rhodecode.lib.str_utils import safe_bytes, safe_str
43 from rhodecode.lib.type_utils import str2bool
41 from ...lib.memory_lru_dict import LRUDict, LRUDictDebug
42 from ...lib.str_utils import safe_bytes, safe_str
43 from ...lib.type_utils import str2bool
44 44
45 45 _default_max_size = 1024
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 class LRUMemoryBackend(memory_backend.MemoryBackend):
51 51 key_prefix = 'lru_mem_backend'
52 52 pickle_values = False
53 53
54 54 def __init__(self, arguments):
55 55 self.max_size = arguments.pop('max_size', _default_max_size)
56 56
57 57 LRUDictClass = LRUDict
58 58 if arguments.pop('log_key_count', None):
59 59 LRUDictClass = LRUDictDebug
60 60
61 61 arguments['cache_dict'] = LRUDictClass(self.max_size)
62 62 super().__init__(arguments)
63 63
64 64 def __repr__(self):
65 65 return f'{self.__class__}(maxsize=`{self.max_size}`)'
66 66
67 67 def __str__(self):
68 68 return self.__repr__()
69 69
70 70 def delete(self, key):
71 71 try:
72 72 del self._cache[key]
73 73 except KeyError:
74 74 # we don't care if key isn't there at deletion
75 75 pass
76 76
77 77 def list_keys(self, prefix):
78 78 return list(self._cache.keys())
79 79
80 80 def delete_multi(self, keys):
81 81 for key in keys:
82 82 self.delete(key)
83 83
84 84 def delete_multi_by_prefix(self, prefix):
85 85 cache_keys = self.list_keys(prefix=prefix)
86 86 num_affected_keys = len(cache_keys)
87 87 if num_affected_keys:
88 88 self.delete_multi(cache_keys)
89 89 return num_affected_keys
90 90
91 91
92 92 class PickleSerializer:
93 93 serializer: None | Serializer = staticmethod( # type: ignore
94 94 functools.partial(pickle.dumps, protocol=pickle.HIGHEST_PROTOCOL)
95 95 )
96 96 deserializer: None | Deserializer = staticmethod( # type: ignore
97 97 functools.partial(pickle.loads)
98 98 )
99 99
100 100
101 101 class MsgPackSerializer:
102 102 serializer: None | Serializer = staticmethod( # type: ignore
103 103 msgpack.packb
104 104 )
105 105 deserializer: None | Deserializer = staticmethod( # type: ignore
106 106 functools.partial(msgpack.unpackb, use_list=False)
107 107 )
108 108
109 109
110 110 class CustomLockFactory(FileLock):
111 111
112 112 @memoized_property
113 113 def _module(self):
114 114
115 115 def gevent_flock(fd, operation):
116 116 """
117 117 Gevent compatible flock
118 118 """
119 119 # set non-blocking, this will cause an exception if we cannot acquire a lock
120 120 operation |= fcntl.LOCK_NB
121 121 start_lock_time = time.time()
122 122 timeout = 60 * 15 # 15min
123 123 while True:
124 124 try:
125 125 flock_org(fd, operation)
126 126 # lock has been acquired
127 127 break
128 128 except (OSError, IOError) as e:
129 129 # raise on other errors than Resource temporarily unavailable
130 130 if e.errno != errno.EAGAIN:
131 131 raise
132 132 elif (time.time() - start_lock_time) > timeout:
133 133 # waited to much time on a lock, better fail than loop for ever
134 134 log.error('Failed to acquire lock on `%s` after waiting %ss',
135 135 self.filename, timeout)
136 136 raise
137 137 wait_timeout = 0.03
138 138 log.debug('Failed to acquire lock on `%s`, retry in %ss',
139 139 self.filename, wait_timeout)
140 140 gevent.sleep(wait_timeout)
141 141
142 142 fcntl.flock = gevent_flock
143 143 return fcntl
144 144
145 145
146 146 class FileNamespaceBackend(PickleSerializer, file_backend.DBMBackend):
147 147 key_prefix = 'file_backend'
148 148
149 149 def __init__(self, arguments):
150 150 arguments['lock_factory'] = CustomLockFactory
151 151 db_file = arguments.get('filename')
152 152
153 153 log.debug('initialing cache-backend=%s db in %s', self.__class__.__name__, db_file)
154 154 db_file_dir = os.path.dirname(db_file)
155 155 if not os.path.isdir(db_file_dir):
156 156 os.makedirs(db_file_dir)
157 157
158 158 try:
159 159 super().__init__(arguments)
160 160 except Exception:
161 161 log.exception('Failed to initialize db at: %s', db_file)
162 162 raise
163 163
164 164 def __repr__(self):
165 165 return f'{self.__class__}(file=`{self.filename}`)'
166 166
167 167 def __str__(self):
168 168 return self.__repr__()
169 169
170 170 def _get_keys_pattern(self, prefix: bytes = b''):
171 171 return b'%b:%b' % (safe_bytes(self.key_prefix), safe_bytes(prefix))
172 172
173 173 def list_keys(self, prefix: bytes = b''):
174 174 prefix = self._get_keys_pattern(prefix)
175 175
176 176 def cond(dbm_key: bytes):
177 177 if not prefix:
178 178 return True
179 179
180 180 if dbm_key.startswith(prefix):
181 181 return True
182 182 return False
183 183
184 184 with self._dbm_file(True) as dbm:
185 185 try:
186 186 return list(filter(cond, dbm.keys()))
187 187 except Exception:
188 188 log.error('Failed to fetch DBM keys from DB: %s', self.get_store())
189 189 raise
190 190
191 191 def delete_multi_by_prefix(self, prefix):
192 192 cache_keys = self.list_keys(prefix=prefix)
193 193 num_affected_keys = len(cache_keys)
194 194 if num_affected_keys:
195 195 self.delete_multi(cache_keys)
196 196 return num_affected_keys
197 197
198 198 def get_store(self):
199 199 return self.filename
200 200
201 def cleanup_store(self):
202 for ext in ("db", "dat", "pag", "dir"):
203 final_filename = self.filename + os.extsep + ext
204 if os.path.exists(final_filename):
205 os.remove(final_filename)
206 log.warning('Removed dbm file %s', final_filename)
207
201 208
202 209 class BaseRedisBackend(redis_backend.RedisBackend):
203 210 key_prefix = ''
204 211
205 212 def __init__(self, arguments):
206 213 self.db_conn = arguments.get('host', '') or arguments.get('url', '') or 'redis-host'
207 214 super().__init__(arguments)
208 215
209 216 self._lock_timeout = self.lock_timeout
210 217 self._lock_auto_renewal = str2bool(arguments.pop("lock_auto_renewal", True))
211 218
212 219 if self._lock_auto_renewal and not self._lock_timeout:
213 220 # set default timeout for auto_renewal
214 221 self._lock_timeout = 30
215 222
216 223 def __repr__(self):
217 224 return f'{self.__class__}(conn=`{self.db_conn}`)'
218 225
219 226 def __str__(self):
220 227 return self.__repr__()
221 228
222 229 def _create_client(self):
223 230 args = {}
224 231
225 232 if self.url is not None:
226 233 args.update(url=self.url)
227 234
228 235 else:
229 236 args.update(
230 237 host=self.host, password=self.password,
231 238 port=self.port, db=self.db
232 239 )
233 240
234 241 connection_pool = redis.ConnectionPool(**args)
235 242 self.writer_client = redis.StrictRedis(
236 243 connection_pool=connection_pool
237 244 )
238 245 self.reader_client = self.writer_client
239 246
240 247 def _get_keys_pattern(self, prefix: bytes = b''):
241 248 return b'%b:%b*' % (safe_bytes(self.key_prefix), safe_bytes(prefix))
242 249
243 250 def list_keys(self, prefix: bytes = b''):
244 251 prefix = self._get_keys_pattern(prefix)
245 252 return self.reader_client.keys(prefix)
246 253
247 254 def delete_multi_by_prefix(self, prefix, use_lua=False):
248 255 if use_lua:
249 256 # high efficient LUA script to delete ALL keys by prefix...
250 257 lua = """local keys = redis.call('keys', ARGV[1])
251 258 for i=1,#keys,5000 do
252 259 redis.call('del', unpack(keys, i, math.min(i+(5000-1), #keys)))
253 260 end
254 261 return #keys"""
255 262 num_affected_keys = self.writer_client.eval(
256 263 lua,
257 264 0,
258 265 f"{prefix}*")
259 266 else:
260 267 cache_keys = self.list_keys(prefix=prefix)
261 268 num_affected_keys = len(cache_keys)
262 269 if num_affected_keys:
263 270 self.delete_multi(cache_keys)
264 271 return num_affected_keys
265 272
266 273 def get_store(self):
267 274 return self.reader_client.connection_pool
268 275
269 276 def get_mutex(self, key):
270 277 if self.distributed_lock:
271 278 lock_key = f'_lock_{safe_str(key)}'
272 279 return get_mutex_lock(
273 280 self.writer_client, lock_key,
274 281 self._lock_timeout,
275 282 auto_renewal=self._lock_auto_renewal
276 283 )
277 284 else:
278 285 return None
279 286
280 287
281 288 class RedisPickleBackend(PickleSerializer, BaseRedisBackend):
282 289 key_prefix = 'redis_pickle_backend'
283 290 pass
284 291
285 292
286 293 class RedisMsgPackBackend(MsgPackSerializer, BaseRedisBackend):
287 294 key_prefix = 'redis_msgpack_backend'
288 295 pass
289 296
290 297
291 298 def get_mutex_lock(client, lock_key, lock_timeout, auto_renewal=False):
292 from rhodecode.lib._vendor import redis_lock
299 from ...lib._vendor import redis_lock
293 300
294 301 class _RedisLockWrapper:
295 302 """LockWrapper for redis_lock"""
296 303
297 304 @classmethod
298 305 def get_lock(cls):
299 306 return redis_lock.Lock(
300 307 redis_client=client,
301 308 name=lock_key,
302 309 expire=lock_timeout,
303 310 auto_renewal=auto_renewal,
304 311 strict=True,
305 312 )
306 313
307 314 def __repr__(self):
308 315 return f"{self.__class__.__name__}:{lock_key}"
309 316
310 317 def __str__(self):
311 318 return f"{self.__class__.__name__}:{lock_key}"
312 319
313 320 def __init__(self):
314 321 self.lock = self.get_lock()
315 322 self.lock_key = lock_key
316 323
317 324 def acquire(self, wait=True):
318 325 log.debug('Trying to acquire Redis lock for key %s', self.lock_key)
319 326 try:
320 327 acquired = self.lock.acquire(wait)
321 328 log.debug('Got lock for key %s, %s', self.lock_key, acquired)
322 329 return acquired
323 330 except redis_lock.AlreadyAcquired:
324 331 return False
325 332 except redis_lock.AlreadyStarted:
326 333 # refresh thread exists, but it also means we acquired the lock
327 334 return True
328 335
329 336 def release(self):
330 337 try:
331 338 self.lock.release()
332 339 except redis_lock.NotAcquired:
333 340 pass
334 341
335 342 return _RedisLockWrapper()
@@ -1,357 +1,357 b''
1 1 # Copyright (C) 2015-2024 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import functools
20 20 import logging
21 21 import os
22 22 import threading
23 23 import time
24 24
25 25 import decorator
26 26 from dogpile.cache import CacheRegion
27 27
28 28 import rhodecode
29 from rhodecode.lib.hash_utils import sha1
30 from rhodecode.lib.str_utils import safe_bytes
31 from rhodecode.lib.type_utils import str2bool # noqa :required by imports from .utils
29 from ...lib.hash_utils import sha1
30 from ...lib.str_utils import safe_bytes
31 from ...lib.type_utils import str2bool # noqa :required by imports from .utils
32 32
33 33 from . import region_meta
34 34
35 35 log = logging.getLogger(__name__)
36 36
37 37
38 38 def isCython(func):
39 39 """
40 40 Private helper that checks if a function is a cython function.
41 41 """
42 42 return func.__class__.__name__ == 'cython_function_or_method'
43 43
44 44
45 45 class RhodeCodeCacheRegion(CacheRegion):
46 46
47 47 def __repr__(self):
48 48 return f'`{self.__class__.__name__}(name={self.name}, backend={self.backend.__class__})`'
49 49
50 50 def conditional_cache_on_arguments(
51 51 self, namespace=None,
52 52 expiration_time=None,
53 53 should_cache_fn=None,
54 54 to_str=str,
55 55 function_key_generator=None,
56 56 condition=True):
57 57 """
58 58 Custom conditional decorator, that will not touch any dogpile internals if
59 59 condition isn't meet. This works a bit different from should_cache_fn
60 60 And it's faster in cases we don't ever want to compute cached values
61 61 """
62 62 expiration_time_is_callable = callable(expiration_time)
63 63 if not namespace:
64 64 namespace = getattr(self, '_default_namespace', None)
65 65
66 66 if function_key_generator is None:
67 67 function_key_generator = self.function_key_generator
68 68
69 69 def get_or_create_for_user_func(func_key_generator, user_func, *arg, **kw):
70 70
71 71 if not condition:
72 72 log.debug('Calling un-cached method:%s', user_func.__name__)
73 73 start = time.time()
74 74 result = user_func(*arg, **kw)
75 75 total = time.time() - start
76 76 log.debug('un-cached method:%s took %.4fs', user_func.__name__, total)
77 77 return result
78 78
79 79 key = func_key_generator(*arg, **kw)
80 80
81 81 timeout = expiration_time() if expiration_time_is_callable \
82 82 else expiration_time
83 83
84 84 log.debug('Calling cached method:`%s`', user_func.__name__)
85 85 return self.get_or_create(key, user_func, timeout, should_cache_fn, (arg, kw))
86 86
87 87 def cache_decorator(user_func):
88 88 if to_str is str:
89 89 # backwards compatible
90 90 key_generator = function_key_generator(namespace, user_func)
91 91 else:
92 92 key_generator = function_key_generator(namespace, user_func, to_str=to_str)
93 93
94 94 def refresh(*arg, **kw):
95 95 """
96 96 Like invalidate, but regenerates the value instead
97 97 """
98 98 key = key_generator(*arg, **kw)
99 99 value = user_func(*arg, **kw)
100 100 self.set(key, value)
101 101 return value
102 102
103 103 def invalidate(*arg, **kw):
104 104 key = key_generator(*arg, **kw)
105 105 self.delete(key)
106 106
107 107 def set_(value, *arg, **kw):
108 108 key = key_generator(*arg, **kw)
109 109 self.set(key, value)
110 110
111 111 def get(*arg, **kw):
112 112 key = key_generator(*arg, **kw)
113 113 return self.get(key)
114 114
115 115 user_func.set = set_
116 116 user_func.invalidate = invalidate
117 117 user_func.get = get
118 118 user_func.refresh = refresh
119 119 user_func.key_generator = key_generator
120 120 user_func.original = user_func
121 121
122 122 # Use `decorate` to preserve the signature of :param:`user_func`.
123 123 return decorator.decorate(user_func, functools.partial(
124 124 get_or_create_for_user_func, key_generator))
125 125
126 126 return cache_decorator
127 127
128 128
129 129 def make_region(*arg, **kw):
130 130 return RhodeCodeCacheRegion(*arg, **kw)
131 131
132 132
133 133 def get_default_cache_settings(settings, prefixes=None):
134 134 prefixes = prefixes or []
135 135 cache_settings = {}
136 136 for key in settings.keys():
137 137 for prefix in prefixes:
138 138 if key.startswith(prefix):
139 139 name = key.split(prefix)[1].strip()
140 140 val = settings[key]
141 141 if isinstance(val, str):
142 142 val = val.strip()
143 143 cache_settings[name] = val
144 144 return cache_settings
145 145
146 146
147 147 def compute_key_from_params(*args):
148 148 """
149 149 Helper to compute key from given params to be used in cache manager
150 150 """
151 151 return sha1(safe_bytes("_".join(map(str, args))))
152 152
153 153
154 154 def custom_key_generator(backend, namespace, fn):
155 155 func_name = fn.__name__
156 156
157 157 def generate_key(*args):
158 158 backend_pref = getattr(backend, 'key_prefix', None) or 'backend_prefix'
159 159 namespace_pref = namespace or 'default_namespace'
160 160 arg_key = compute_key_from_params(*args)
161 161 final_key = f"{backend_pref}:{namespace_pref}:{func_name}_{arg_key}"
162 162
163 163 return final_key
164 164
165 165 return generate_key
166 166
167 167
168 168 def backend_key_generator(backend):
169 169 """
170 170 Special wrapper that also sends over the backend to the key generator
171 171 """
172 172 def wrapper(namespace, fn):
173 173 return custom_key_generator(backend, namespace, fn)
174 174 return wrapper
175 175
176 176
177 177 def get_or_create_region(region_name, region_namespace: str = None, use_async_runner=False):
178 178 from .backends import FileNamespaceBackend
179 179 from . import async_creation_runner
180 180
181 181 region_obj = region_meta.dogpile_cache_regions.get(region_name)
182 182 if not region_obj:
183 183 reg_keys = list(region_meta.dogpile_cache_regions.keys())
184 184 raise OSError(f'Region `{region_name}` not in configured: {reg_keys}.')
185 185
186 186 region_uid_name = f'{region_name}:{region_namespace}'
187 187
188 188 # Special case for ONLY the FileNamespaceBackend backend. We register one-file-per-region
189 189 if isinstance(region_obj.actual_backend, FileNamespaceBackend):
190 190 if not region_namespace:
191 191 raise ValueError(f'{FileNamespaceBackend} used requires to specify region_namespace param')
192 192
193 193 region_exist = region_meta.dogpile_cache_regions.get(region_namespace)
194 194 if region_exist:
195 195 log.debug('Using already configured region: %s', region_namespace)
196 196 return region_exist
197 197
198 198 expiration_time = region_obj.expiration_time
199 199
200 200 cache_dir = region_meta.dogpile_config_defaults['cache_dir']
201 201 namespace_cache_dir = cache_dir
202 202
203 203 # we default the namespace_cache_dir to our default cache dir.
204 204 # however, if this backend is configured with filename= param, we prioritize that
205 205 # so all caches within that particular region, even those namespaced end up in the same path
206 206 if region_obj.actual_backend.filename:
207 207 namespace_cache_dir = os.path.dirname(region_obj.actual_backend.filename)
208 208
209 209 if not os.path.isdir(namespace_cache_dir):
210 210 os.makedirs(namespace_cache_dir)
211 211 new_region = make_region(
212 212 name=region_uid_name,
213 213 function_key_generator=backend_key_generator(region_obj.actual_backend)
214 214 )
215 215
216 216 namespace_filename = os.path.join(
217 217 namespace_cache_dir, f"{region_name}_{region_namespace}.cache_db")
218 218 # special type that allows 1db per namespace
219 219 new_region.configure(
220 220 backend='dogpile.cache.rc.file_namespace',
221 221 expiration_time=expiration_time,
222 222 arguments={"filename": namespace_filename}
223 223 )
224 224
225 225 # create and save in region caches
226 226 log.debug('configuring new region: %s', region_uid_name)
227 227 region_obj = region_meta.dogpile_cache_regions[region_namespace] = new_region
228 228
229 229 region_obj._default_namespace = region_namespace
230 230 if use_async_runner:
231 231 region_obj.async_creation_runner = async_creation_runner
232 232 return region_obj
233 233
234 234
235 235 def clear_cache_namespace(cache_region: str | RhodeCodeCacheRegion, cache_namespace_uid: str, method: str) -> int:
236 236 from . import CLEAR_DELETE, CLEAR_INVALIDATE
237 237
238 238 if not isinstance(cache_region, RhodeCodeCacheRegion):
239 239 cache_region = get_or_create_region(cache_region, cache_namespace_uid)
240 240 log.debug('clearing cache region: %s [prefix:%s] with method=%s',
241 241 cache_region, cache_namespace_uid, method)
242 242
243 243 num_affected_keys = 0
244 244
245 245 if method == CLEAR_INVALIDATE:
246 246 # NOTE: The CacheRegion.invalidate() method’s default mode of
247 247 # operation is to set a timestamp local to this CacheRegion in this Python process only.
248 248 # It does not impact other Python processes or regions as the timestamp is only stored locally in memory.
249 249 cache_region.invalidate(hard=True)
250 250
251 251 if method == CLEAR_DELETE:
252 252 num_affected_keys = cache_region.backend.delete_multi_by_prefix(prefix=cache_namespace_uid)
253 253 return num_affected_keys
254 254
255 255
256 256 class ActiveRegionCache(object):
257 257 def __init__(self, context, cache_data: dict):
258 258 self.context = context
259 259 self.cache_data = cache_data
260 260
261 261 @property
262 262 def state_uid(self) -> str:
263 263 return self.cache_data['cache_state_uid']
264 264
265 265
266 266 class InvalidationContext(object):
267 267 """
268 268 usage::
269 269
270 270 from rhodecode.lib import rc_cache
271 271
272 272 repo_namespace_key = 'some-cache-for-repo-id-100'
273 273 inv_context_manager = rc_cache.InvalidationContext(key=repo_namespace_key)
274 274
275 275 def cache_generator(_state_uid):
276 276
277 277 @region.conditional_cache_on_arguments(namespace='some-common-namespace-100')
278 278 def _dummy_func(*args):
279 279 # compute heavy function
280 280 return _state_uid, 'result'
281 281
282 282 return _dummy_func
283 283
284 284 with inv_context_manager as invalidation_context:
285 285 cache_state_uid = invalidation_context.state_uid
286 286 cache_func = cache_generator(cache_state_uid)
287 287 previous_state_uid, result = cache_func(*call_args)
288 288
289 289 should_invalidate = previous_state_uid != cache_state_uid
290 290 if should_invalidate:
291 291 _, result = cache_func.refresh(*call_args)
292 292
293 293 # To send global invalidation signal, simply run
294 294 CacheKey.set_invalidate(repo_namespace_key)
295 295
296 296 """
297 297
298 298 def __repr__(self):
299 299 return f'<InvalidationContext:{self.cache_key}>'
300 300
301 301 def __init__(self, key, raise_exception=False, thread_scoped=None):
302 302 self.cache_key = key
303 303
304 304 self.raise_exception = raise_exception
305 305 self.proc_id = rhodecode.ConfigGet().get_str('instance_id') or 'DEFAULT'
306 306 self.thread_id = 'global'
307 307
308 308 if thread_scoped is None:
309 309 # if we set "default" we can override this via .ini settings
310 310 thread_scoped = rhodecode.ConfigGet().get_bool('cache_thread_scoped')
311 311
312 312 # Append the thread id to the cache key if this invalidation context
313 313 # should be scoped to the current thread.
314 314 if thread_scoped is True:
315 315 self.thread_id = threading.current_thread().ident
316 316
317 317 self.proc_key = f'proc:{self.proc_id}|thread:{self.thread_id}|key:{self.cache_key}'
318 318 self.compute_time = 0
319 319
320 320 def get_or_create_cache_obj(self):
321 321 from rhodecode.model.db import CacheKey, Session, IntegrityError
322 322
323 323 cache_obj = CacheKey.get_active_cache(self.cache_key)
324 324 log.debug('Fetched cache obj %s using %s cache key.', cache_obj, self.cache_key)
325 325
326 326 if not cache_obj:
327 327 # generate new UID for non-existing cache object
328 328 cache_state_uid = CacheKey.generate_new_state_uid()
329 329 cache_obj = CacheKey(self.cache_key, cache_args=f'repo_state:{self._start_time}',
330 330 cache_state_uid=cache_state_uid, cache_active=True)
331 331 try:
332 332 Session().add(cache_obj)
333 333 Session().commit()
334 334 except IntegrityError:
335 335 # if we catch integrity error, it means we inserted this object
336 336 # assumption is that's really an edge race-condition case and
337 337 # it's safe is to skip it
338 338 Session().rollback()
339 339 except Exception:
340 340 log.exception('Failed to commit on cache key update')
341 341 Session().rollback()
342 342 if self.raise_exception:
343 343 raise
344 344 return cache_obj
345 345
346 346 def __enter__(self):
347 347 log.debug('Entering cache invalidation check context: %s', self)
348 348 self._start_time = time.time()
349 349
350 350 self.cache_obj = self.get_or_create_cache_obj()
351 351 cache_data = self.cache_obj.get_dict()
352 352
353 353 return ActiveRegionCache(context=self, cache_data=cache_data)
354 354
355 355 def __exit__(self, exc_type, exc_val, exc_tb):
356 356 # save compute time
357 357 self.compute_time = time.time() - self._start_time
@@ -1,105 +1,104 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import sys
20 20 import logging
21 21
22 22 import click
23 23
24 24 from rhodecode.lib.pyramid_utils import bootstrap
25 25 from rhodecode.model.db import Session, User, Repository
26 26 from rhodecode.model.user import UserModel
27 27 from rhodecode.apps.file_store import utils as store_utils
28 28
29 29 log = logging.getLogger(__name__)
30 30
31 31
32 32 @click.command()
33 33 @click.argument('ini_path', type=click.Path(exists=True))
34 34 @click.option(
35 35 '--filename',
36 36 required=True,
37 37 help='Filename for artifact.')
38 38 @click.option(
39 39 '--file-path',
40 40 required=True,
41 41 type=click.Path(exists=True, dir_okay=False, readable=True),
42 42 help='Path to a file to be added as artifact')
43 43 @click.option(
44 44 '--repo-id',
45 45 required=True,
46 46 type=int,
47 47 help='ID of repository to add this artifact to.')
48 48 @click.option(
49 49 '--user-id',
50 50 default=None,
51 51 type=int,
52 52 help='User ID for creator of artifact. '
53 53 'Default would be first super admin.')
54 54 @click.option(
55 55 '--description',
56 56 default=None,
57 57 type=str,
58 58 help='Add description to this artifact')
59 59 def main(ini_path, filename, file_path, repo_id, user_id, description):
60 60 return command(ini_path, filename, file_path, repo_id, user_id, description)
61 61
62 62
63 63 def command(ini_path, filename, file_path, repo_id, user_id, description):
64 64 with bootstrap(ini_path, env={'RC_CMD_SETUP_RC': '1'}) as env:
65 65 try:
66 66 from rc_ee.api.views.store_api import _store_file
67 67 except ImportError:
68 68 click.secho('ERROR: Unable to import store_api. '
69 69 'store_api is only available in EE edition of RhodeCode',
70 70 fg='red')
71 71 sys.exit(-1)
72 72
73 73 request = env['request']
74 74
75 75 repo = Repository.get(repo_id)
76 76 if not repo:
77 77 click.secho(f'ERROR: Unable to find repository with id `{repo_id}`',
78 78 fg='red')
79 79 sys.exit(-1)
80 80
81 81 # if we don't give user, or it's "DEFAULT" user we pick super-admin
82 82 if user_id is not None or user_id == 1:
83 83 db_user = User.get(user_id)
84 84 else:
85 85 db_user = User.get_first_super_admin()
86 86
87 87 if not db_user:
88 88 click.secho(f'ERROR: Unable to find user with id/username `{user_id}`',
89 89 fg='red')
90 90 sys.exit(-1)
91 91
92 92 auth_user = db_user.AuthUser(ip_addr='127.0.0.1')
93 93
94 storage = store_utils.get_file_storage(request.registry.settings)
94 f_store = store_utils.get_filestore_backend(request.registry.settings)
95 95
96 96 with open(file_path, 'rb') as f:
97 97 click.secho(f'Adding new artifact from path: `{file_path}`',
98 98 fg='green')
99 99
100 100 file_data = _store_file(
101 storage, auth_user, filename, content=None, check_acl=True,
101 f_store, auth_user, filename, content=None, check_acl=True,
102 102 file_obj=f, description=description,
103 103 scope_repo_id=repo.repo_id)
104 click.secho(f'File Data: {file_data}',
105 fg='green')
104 click.secho(f'File Data: {file_data}', fg='green')
@@ -1,124 +1,123 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18 import logging
19 19
20 20 import click
21 21 import pyramid.paster
22 22
23 23 from rhodecode.lib.pyramid_utils import bootstrap
24 24 from rhodecode.lib.config_utils import get_app_config
25 25 from rhodecode.lib.db_manage import DbManage
26 26 from rhodecode.lib.utils2 import get_encryption_key
27 27 from rhodecode.model.db import Session
28 28
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32
33 33 @click.command()
34 34 @click.argument('ini_path', type=click.Path(exists=True))
35 35 @click.option(
36 36 '--force-yes/--force-no', default=None,
37 37 help="Force yes/no to every question")
38 38 @click.option(
39 39 '--user',
40 40 default=None,
41 41 help='Initial super-admin username')
42 42 @click.option(
43 43 '--email',
44 44 default=None,
45 45 help='Initial super-admin email address.')
46 46 @click.option(
47 47 '--password',
48 48 default=None,
49 49 help='Initial super-admin password. Minimum 6 chars.')
50 50 @click.option(
51 51 '--api-key',
52 52 help='Initial API key for the admin user')
53 53 @click.option(
54 54 '--repos',
55 55 default=None,
56 56 help='Absolute path to storage location. This is storage for all '
57 57 'existing and future repositories, and repository groups.')
58 58 @click.option(
59 59 '--public-access/--no-public-access',
60 60 default=None,
61 61 help='Enable public access on this installation. '
62 62 'Default is public access enabled.')
63 63 @click.option(
64 64 '--skip-existing-db',
65 65 default=False,
66 66 is_flag=True,
67 67 help='Do not destroy and re-initialize the database if it already exist.')
68 68 @click.option(
69 69 '--apply-license-key',
70 70 default=False,
71 71 is_flag=True,
72 72 help='Get the license key from a license file or ENV and apply during DB creation.')
73 73 def main(ini_path, force_yes, user, email, password, api_key, repos,
74 74 public_access, skip_existing_db, apply_license_key):
75 75 return command(ini_path, force_yes, user, email, password, api_key,
76 76 repos, public_access, skip_existing_db, apply_license_key)
77 77
78 78
79 79 def command(ini_path, force_yes, user, email, password, api_key, repos,
80 80 public_access, skip_existing_db, apply_license_key):
81 81 # mapping of old parameters to new CLI from click
82 82 options = dict(
83 83 username=user,
84 84 email=email,
85 85 password=password,
86 86 api_key=api_key,
87 87 repos_location=repos,
88 88 force_ask=force_yes,
89 89 public_access=public_access
90 90 )
91 91 pyramid.paster.setup_logging(ini_path)
92 92
93 93 config = get_app_config(ini_path)
94 94
95 95 db_uri = config['sqlalchemy.db1.url']
96 96 enc_key = get_encryption_key(config)
97 97 dbmanage = DbManage(log_sql=True, dbconf=db_uri, root='.',
98 98 tests=False, cli_args=options, enc_key=enc_key)
99 99 if skip_existing_db and dbmanage.db_exists():
100 100 return
101 101
102 102 dbmanage.create_tables(override=True)
103 103 dbmanage.set_db_version()
104 104 opts = dbmanage.config_prompt(None)
105 105 dbmanage.create_settings(opts)
106 106 dbmanage.create_default_user()
107 107 dbmanage.create_admin_and_prompt()
108 108 dbmanage.create_permissions()
109 109 dbmanage.populate_default_permissions()
110 110 if apply_license_key:
111 try:
112 from rc_license.models import apply_trial_license_if_missing
113 apply_trial_license_if_missing(force=True)
114 except ImportError:
115 pass
111 from rhodecode.model.license import apply_license_from_file
112 license_file_path = config.get('license.import_path')
113 if license_file_path:
114 apply_license_from_file(license_file_path, force=True)
116 115
117 116 Session().commit()
118 117
119 118 with bootstrap(ini_path, env={'RC_CMD_SETUP_RC': '1'}) as env:
120 119 msg = 'Successfully initialized database, schema and default data.'
121 120 print()
122 121 print('*' * len(msg))
123 122 print(msg.upper())
124 123 print('*' * len(msg))
@@ -1,183 +1,187 b''
1 1 # Copyright (C) 2011-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import typing
20 20 import base64
21 21 import logging
22 22 from unidecode import unidecode
23 23
24 24 import rhodecode
25 25 from rhodecode.lib.type_utils import aslist
26 26
27 27
28 28 log = logging.getLogger(__name__)
29 29
30 30
31 31 def safe_int(val, default=None) -> int:
32 32 """
33 33 Returns int() of val if val is not convertable to int use default
34 34 instead
35 35
36 36 :param val:
37 37 :param default:
38 38 """
39 39
40 40 try:
41 41 val = int(val)
42 42 except (ValueError, TypeError):
43 43 val = default
44 44
45 45 return val
46 46
47 47
48 48 def safe_float(val, default=None) -> float:
49 49 """
50 50 Returns float() of val if val is not convertable to float use default
51 51 instead
52 52
53 53 :param val:
54 54 :param default:
55 55 """
56 56
57 57 try:
58 58 val = float(val)
59 59 except (ValueError, TypeError):
60 60 val = default
61 61
62 62 return val
63 63
64 64
65 65 def base64_to_str(text: str | bytes) -> str:
66 66 return safe_str(base64.encodebytes(safe_bytes(text))).strip()
67 67
68 68
69 69 def get_default_encodings() -> list[str]:
70 70 return aslist(rhodecode.CONFIG.get('default_encoding', 'utf8'), sep=',')
71 71
72 72
73 73 DEFAULT_ENCODINGS = get_default_encodings()
74 74
75 75
76 76 def safe_str(str_, to_encoding=None) -> str:
77 77 """
78 78 safe str function. Does few trick to turn unicode_ into string
79 79
80 80 :param str_: str to encode
81 81 :param to_encoding: encode to this type UTF8 default
82 82 """
83 83 if isinstance(str_, str):
84 84 return str_
85 85
86 86 # if it's bytes cast to str
87 87 if not isinstance(str_, bytes):
88 88 return str(str_)
89 89
90 90 to_encoding = to_encoding or DEFAULT_ENCODINGS
91 91 if not isinstance(to_encoding, (list, tuple)):
92 92 to_encoding = [to_encoding]
93 93
94 94 for enc in to_encoding:
95 95 try:
96 96 return str(str_, enc)
97 97 except UnicodeDecodeError:
98 98 pass
99 99
100 100 return str(str_, to_encoding[0], 'replace')
101 101
102 102
103 103 def safe_bytes(str_, from_encoding=None) -> bytes:
104 104 """
105 105 safe bytes function. Does few trick to turn str_ into bytes string:
106 106
107 107 :param str_: string to decode
108 108 :param from_encoding: encode from this type UTF8 default
109 109 """
110 110 if isinstance(str_, bytes):
111 111 return str_
112 112
113 113 if not isinstance(str_, str):
114 114 raise ValueError(f'safe_bytes cannot convert other types than str: got: {type(str_)}')
115 115
116 116 from_encoding = from_encoding or get_default_encodings()
117 117 if not isinstance(from_encoding, (list, tuple)):
118 118 from_encoding = [from_encoding]
119 119
120 120 for enc in from_encoding:
121 121 try:
122 122 return str_.encode(enc)
123 123 except UnicodeDecodeError:
124 124 pass
125 125
126 126 return str_.encode(from_encoding[0], 'replace')
127 127
128 128
129 129 def ascii_bytes(str_, allow_bytes=False) -> bytes:
130 130 """
131 131 Simple conversion from str to bytes, with assumption that str_ is pure ASCII.
132 132 Fails with UnicodeError on invalid input.
133 133 This should be used where encoding and "safe" ambiguity should be avoided.
134 134 Where strings already have been encoded in other ways but still are unicode
135 135 string - for example to hex, base64, json, urlencoding, or are known to be
136 136 identifiers.
137 137 """
138 138 if allow_bytes and isinstance(str_, bytes):
139 139 return str_
140 140
141 141 if not isinstance(str_, str):
142 142 raise ValueError(f'ascii_bytes cannot convert other types than str: got: {type(str_)}')
143 143 return str_.encode('ascii')
144 144
145 145
146 146 def ascii_str(str_) -> str:
147 147 """
148 148 Simple conversion from bytes to str, with assumption that str_ is pure ASCII.
149 149 Fails with UnicodeError on invalid input.
150 150 This should be used where encoding and "safe" ambiguity should be avoided.
151 151 Where strings are encoded but also in other ways are known to be ASCII, and
152 152 where a unicode string is wanted without caring about encoding. For example
153 153 to hex, base64, urlencoding, or are known to be identifiers.
154 154 """
155 155
156 156 if not isinstance(str_, bytes):
157 157 raise ValueError(f'ascii_str cannot convert other types than bytes: got: {type(str_)}')
158 158 return str_.decode('ascii')
159 159
160 160
161 161 def convert_special_chars(str_) -> str:
162 162 """
163 163 trie to replace non-ascii letters to their ascii representation eg::
164 164
165 165 `żołw` converts into `zolw`
166 166 """
167 167 value = safe_str(str_)
168 168 converted_value = unidecode(value)
169 169 return converted_value
170 170
171 171
172 172 def splitnewlines(text: bytes):
173 173 """
174 174 like splitlines, but only split on newlines.
175 175 """
176 176
177 177 lines = [_l + b'\n' for _l in text.split(b'\n')]
178 178 if lines:
179 179 if lines[-1] == b'\n':
180 180 lines.pop()
181 181 else:
182 182 lines[-1] = lines[-1][:-1]
183 183 return lines
184
185
186 def header_safe_str(val):
187 return safe_bytes(val).decode('latin-1', errors='replace')
@@ -1,827 +1,866 b''
1 1 # Copyright (C) 2017-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 import os
21 21 import sys
22 22 import time
23 23 import platform
24 24 import collections
25 25 import psutil
26 26 from functools import wraps
27 27
28 28 import pkg_resources
29 29 import logging
30 30 import resource
31 31
32 32 import configparser
33 33
34 34 from rc_license.models import LicenseModel
35 35 from rhodecode.lib.str_utils import safe_str
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 _NA = 'NOT AVAILABLE'
41 41 _NA_FLOAT = 0.0
42 42
43 43 STATE_OK = 'ok'
44 44 STATE_ERR = 'error'
45 45 STATE_WARN = 'warning'
46 46
47 47 STATE_OK_DEFAULT = {'message': '', 'type': STATE_OK}
48 48
49 49
50 50 registered_helpers = {}
51 51
52 52
53 53 def register_sysinfo(func):
54 54 """
55 55 @register_helper
56 56 def db_check():
57 57 pass
58 58
59 59 db_check == registered_helpers['db_check']
60 60 """
61 61 global registered_helpers
62 62 registered_helpers[func.__name__] = func
63 63
64 64 @wraps(func)
65 65 def _wrapper(*args, **kwargs):
66 66 return func(*args, **kwargs)
67 67 return _wrapper
68 68
69 69
70 70 # HELPERS
71 71 def percentage(part: (int, float), whole: (int, float)):
72 72 whole = float(whole)
73 73 if whole > 0:
74 74 return round(100 * float(part) / whole, 1)
75 75 return 0.0
76 76
77 77
78 78 def get_storage_size(storage_path):
79 79 sizes = []
80 80 for file_ in os.listdir(storage_path):
81 81 storage_file = os.path.join(storage_path, file_)
82 82 if os.path.isfile(storage_file):
83 83 try:
84 84 sizes.append(os.path.getsize(storage_file))
85 85 except OSError:
86 86 log.exception('Failed to get size of storage file %s', storage_file)
87 87 pass
88 88
89 89 return sum(sizes)
90 90
91 91
92 92 def get_resource(resource_type):
93 93 try:
94 94 return resource.getrlimit(resource_type)
95 95 except Exception:
96 96 return 'NOT_SUPPORTED'
97 97
98 98
99 99 def get_cert_path(ini_path):
100 100 default = '/etc/ssl/certs/ca-certificates.crt'
101 101 control_ca_bundle = os.path.join(
102 102 os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(ini_path)))),
103 103 '/etc/ssl/certs/ca-certificates.crt')
104 104 if os.path.isfile(control_ca_bundle):
105 105 default = control_ca_bundle
106 106
107 107 return default
108 108
109 109
110 110 class SysInfoRes(object):
111 111 def __init__(self, value, state=None, human_value=None):
112 112 self.value = value
113 113 self.state = state or STATE_OK_DEFAULT
114 114 self.human_value = human_value or value
115 115
116 116 def __json__(self):
117 117 return {
118 118 'value': self.value,
119 119 'state': self.state,
120 120 'human_value': self.human_value,
121 121 }
122 122
123 123 def get_value(self):
124 124 return self.__json__()
125 125
126 126 def __str__(self):
127 127 return f'<SysInfoRes({self.__json__()})>'
128 128
129 129
130 130 class SysInfo(object):
131 131
132 132 def __init__(self, func_name, **kwargs):
133 133 self.function_name = func_name
134 134 self.value = _NA
135 135 self.state = None
136 136 self.kwargs = kwargs or {}
137 137
138 138 def __call__(self):
139 139 computed = self.compute(**self.kwargs)
140 140 if not isinstance(computed, SysInfoRes):
141 141 raise ValueError(
142 142 'computed value for {} is not instance of '
143 143 '{}, got {} instead'.format(
144 144 self.function_name, SysInfoRes, type(computed)))
145 145 return computed.__json__()
146 146
147 147 def __str__(self):
148 148 return f'<SysInfo({self.function_name})>'
149 149
150 150 def compute(self, **kwargs):
151 151 return self.function_name(**kwargs)
152 152
153 153
154 154 # SysInfo functions
155 155 @register_sysinfo
156 156 def python_info():
157 157 value = dict(version=f'{platform.python_version()}:{platform.python_implementation()}',
158 158 executable=sys.executable)
159 159 return SysInfoRes(value=value)
160 160
161 161
162 162 @register_sysinfo
163 163 def py_modules():
164 164 mods = dict([(p.project_name, {'version': p.version, 'location': p.location})
165 165 for p in pkg_resources.working_set])
166 166
167 167 value = sorted(mods.items(), key=lambda k: k[0].lower())
168 168 return SysInfoRes(value=value)
169 169
170 170
171 171 @register_sysinfo
172 172 def platform_type():
173 173 from rhodecode.lib.utils import generate_platform_uuid
174 174
175 175 value = dict(
176 176 name=safe_str(platform.platform()),
177 177 uuid=generate_platform_uuid()
178 178 )
179 179 return SysInfoRes(value=value)
180 180
181 181
182 182 @register_sysinfo
183 183 def locale_info():
184 184 import locale
185 185
186 186 def safe_get_locale(locale_name):
187 187 try:
188 188 locale.getlocale(locale_name)
189 189 except TypeError:
190 190 return f'FAILED_LOCALE_GET:{locale_name}'
191 191
192 192 value = dict(
193 193 locale_default=locale.getlocale(),
194 194 locale_lc_all=safe_get_locale(locale.LC_ALL),
195 195 locale_lc_ctype=safe_get_locale(locale.LC_CTYPE),
196 196 lang_env=os.environ.get('LANG'),
197 197 lc_all_env=os.environ.get('LC_ALL'),
198 198 local_archive_env=os.environ.get('LOCALE_ARCHIVE'),
199 199 )
200 200 human_value = \
201 201 f"LANG: {value['lang_env']}, \
202 202 locale LC_ALL: {value['locale_lc_all']}, \
203 203 locale LC_CTYPE: {value['locale_lc_ctype']}, \
204 204 Default locales: {value['locale_default']}"
205 205
206 206 return SysInfoRes(value=value, human_value=human_value)
207 207
208 208
209 209 @register_sysinfo
210 210 def ulimit_info():
211 211 data = collections.OrderedDict([
212 212 ('cpu time (seconds)', get_resource(resource.RLIMIT_CPU)),
213 213 ('file size', get_resource(resource.RLIMIT_FSIZE)),
214 214 ('stack size', get_resource(resource.RLIMIT_STACK)),
215 215 ('core file size', get_resource(resource.RLIMIT_CORE)),
216 216 ('address space size', get_resource(resource.RLIMIT_AS)),
217 217 ('locked in mem size', get_resource(resource.RLIMIT_MEMLOCK)),
218 218 ('heap size', get_resource(resource.RLIMIT_DATA)),
219 219 ('rss size', get_resource(resource.RLIMIT_RSS)),
220 220 ('number of processes', get_resource(resource.RLIMIT_NPROC)),
221 221 ('open files', get_resource(resource.RLIMIT_NOFILE)),
222 222 ])
223 223
224 224 text = ', '.join(f'{k}:{v}' for k, v in data.items())
225 225
226 226 value = {
227 227 'limits': data,
228 228 'text': text,
229 229 }
230 230 return SysInfoRes(value=value)
231 231
232 232
233 233 @register_sysinfo
234 234 def uptime():
235 235 from rhodecode.lib.helpers import age, time_to_datetime
236 236 from rhodecode.translation import TranslationString
237 237
238 238 value = dict(boot_time=0, uptime=0, text='')
239 239 state = STATE_OK_DEFAULT
240 240
241 241 boot_time = psutil.boot_time()
242 242 value['boot_time'] = boot_time
243 243 value['uptime'] = time.time() - boot_time
244 244
245 245 date_or_age = age(time_to_datetime(boot_time))
246 246 if isinstance(date_or_age, TranslationString):
247 247 date_or_age = date_or_age.interpolate()
248 248
249 249 human_value = value.copy()
250 250 human_value['boot_time'] = time_to_datetime(boot_time)
251 251 human_value['uptime'] = age(time_to_datetime(boot_time), show_suffix=False)
252 252
253 253 human_value['text'] = f'Server started {date_or_age}'
254 254 return SysInfoRes(value=value, human_value=human_value)
255 255
256 256
257 257 @register_sysinfo
258 258 def memory():
259 259 from rhodecode.lib.helpers import format_byte_size_binary
260 260 value = dict(available=0, used=0, used_real=0, cached=0, percent=0,
261 261 percent_used=0, free=0, inactive=0, active=0, shared=0,
262 262 total=0, buffers=0, text='')
263 263
264 264 state = STATE_OK_DEFAULT
265 265
266 266 value.update(dict(psutil.virtual_memory()._asdict()))
267 267 value['used_real'] = value['total'] - value['available']
268 268 value['percent_used'] = psutil._common.usage_percent(value['used_real'], value['total'], 1)
269 269
270 270 human_value = value.copy()
271 271 human_value['text'] = '{}/{}, {}% used'.format(
272 272 format_byte_size_binary(value['used_real']),
273 273 format_byte_size_binary(value['total']),
274 274 value['percent_used'])
275 275
276 276 keys = list(value.keys())[::]
277 277 keys.pop(keys.index('percent'))
278 278 keys.pop(keys.index('percent_used'))
279 279 keys.pop(keys.index('text'))
280 280 for k in keys:
281 281 human_value[k] = format_byte_size_binary(value[k])
282 282
283 283 if state['type'] == STATE_OK and value['percent_used'] > 90:
284 284 msg = 'Critical: your available RAM memory is very low.'
285 285 state = {'message': msg, 'type': STATE_ERR}
286 286
287 287 elif state['type'] == STATE_OK and value['percent_used'] > 70:
288 288 msg = 'Warning: your available RAM memory is running low.'
289 289 state = {'message': msg, 'type': STATE_WARN}
290 290
291 291 return SysInfoRes(value=value, state=state, human_value=human_value)
292 292
293 293
294 294 @register_sysinfo
295 295 def machine_load():
296 296 value = {'1_min': _NA_FLOAT, '5_min': _NA_FLOAT, '15_min': _NA_FLOAT, 'text': ''}
297 297 state = STATE_OK_DEFAULT
298 298
299 299 # load averages
300 300 if hasattr(psutil.os, 'getloadavg'):
301 301 value.update(dict(
302 302 list(zip(['1_min', '5_min', '15_min'], psutil.os.getloadavg()))
303 303 ))
304 304
305 305 human_value = value.copy()
306 306 human_value['text'] = '1min: {}, 5min: {}, 15min: {}'.format(
307 307 value['1_min'], value['5_min'], value['15_min'])
308 308
309 309 if state['type'] == STATE_OK and value['15_min'] > 5.0:
310 310 msg = 'Warning: your machine load is very high.'
311 311 state = {'message': msg, 'type': STATE_WARN}
312 312
313 313 return SysInfoRes(value=value, state=state, human_value=human_value)
314 314
315 315
316 316 @register_sysinfo
317 317 def cpu():
318 318 value = {'cpu': 0, 'cpu_count': 0, 'cpu_usage': []}
319 319 state = STATE_OK_DEFAULT
320 320
321 321 value['cpu'] = psutil.cpu_percent(0.5)
322 322 value['cpu_usage'] = psutil.cpu_percent(0.5, percpu=True)
323 323 value['cpu_count'] = psutil.cpu_count()
324 324
325 325 human_value = value.copy()
326 326 human_value['text'] = f'{value["cpu_count"]} cores at {value["cpu"]} %'
327 327
328 328 return SysInfoRes(value=value, state=state, human_value=human_value)
329 329
330 330
331 331 @register_sysinfo
332 332 def storage():
333 333 from rhodecode.lib.helpers import format_byte_size_binary
334 334 from rhodecode.lib.utils import get_rhodecode_repo_store_path
335 335 path = get_rhodecode_repo_store_path()
336 336
337 337 value = dict(percent=0, used=0, total=0, path=path, text='')
338 338 state = STATE_OK_DEFAULT
339 339
340 340 try:
341 341 value.update(dict(psutil.disk_usage(path)._asdict()))
342 342 except Exception as e:
343 343 log.exception('Failed to fetch disk info')
344 344 state = {'message': str(e), 'type': STATE_ERR}
345 345
346 346 human_value = value.copy()
347 347 human_value['used'] = format_byte_size_binary(value['used'])
348 348 human_value['total'] = format_byte_size_binary(value['total'])
349 349 human_value['text'] = "{}/{}, {}% used".format(
350 350 format_byte_size_binary(value['used']),
351 351 format_byte_size_binary(value['total']),
352 352 value['percent'])
353 353
354 354 if state['type'] == STATE_OK and value['percent'] > 90:
355 355 msg = 'Critical: your disk space is very low.'
356 356 state = {'message': msg, 'type': STATE_ERR}
357 357
358 358 elif state['type'] == STATE_OK and value['percent'] > 70:
359 359 msg = 'Warning: your disk space is running low.'
360 360 state = {'message': msg, 'type': STATE_WARN}
361 361
362 362 return SysInfoRes(value=value, state=state, human_value=human_value)
363 363
364 364
365 365 @register_sysinfo
366 366 def storage_inodes():
367 367 from rhodecode.lib.utils import get_rhodecode_repo_store_path
368 368 path = get_rhodecode_repo_store_path()
369 369
370 370 value = dict(percent=0.0, free=0, used=0, total=0, path=path, text='')
371 371 state = STATE_OK_DEFAULT
372 372
373 373 try:
374 374 i_stat = os.statvfs(path)
375 375 value['free'] = i_stat.f_ffree
376 376 value['used'] = i_stat.f_files-i_stat.f_favail
377 377 value['total'] = i_stat.f_files
378 378 value['percent'] = percentage(value['used'], value['total'])
379 379 except Exception as e:
380 380 log.exception('Failed to fetch disk inodes info')
381 381 state = {'message': str(e), 'type': STATE_ERR}
382 382
383 383 human_value = value.copy()
384 384 human_value['text'] = "{}/{}, {}% used".format(
385 385 value['used'], value['total'], value['percent'])
386 386
387 387 if state['type'] == STATE_OK and value['percent'] > 90:
388 388 msg = 'Critical: your disk free inodes are very low.'
389 389 state = {'message': msg, 'type': STATE_ERR}
390 390
391 391 elif state['type'] == STATE_OK and value['percent'] > 70:
392 392 msg = 'Warning: your disk free inodes are running low.'
393 393 state = {'message': msg, 'type': STATE_WARN}
394 394
395 395 return SysInfoRes(value=value, state=state, human_value=human_value)
396 396
397 397
398 398 @register_sysinfo
399 def storage_archives():
399 def storage_artifacts():
400 400 import rhodecode
401 401 from rhodecode.lib.helpers import format_byte_size_binary
402 402 from rhodecode.lib.archive_cache import get_archival_cache_store
403 403
404 storage_type = rhodecode.ConfigGet().get_str('archive_cache.backend.type')
404 backend_type = rhodecode.ConfigGet().get_str('archive_cache.backend.type')
405 405
406 value = dict(percent=0, used=0, total=0, items=0, path='', text='', type=storage_type)
406 value = dict(percent=0, used=0, total=0, items=0, path='', text='', type=backend_type)
407 407 state = STATE_OK_DEFAULT
408 408 try:
409 409 d_cache = get_archival_cache_store(config=rhodecode.CONFIG)
410 backend_type = str(d_cache)
410 411
411 412 total_files, total_size, _directory_stats = d_cache.get_statistics()
412 413
413 414 value.update({
414 415 'percent': 100,
415 416 'used': total_size,
416 417 'total': total_size,
417 418 'items': total_files,
418 'path': d_cache.storage_path
419 'path': d_cache.storage_path,
420 'type': backend_type
419 421 })
420 422
421 423 except Exception as e:
422 424 log.exception('failed to fetch archive cache storage')
423 425 state = {'message': str(e), 'type': STATE_ERR}
424 426
425 427 human_value = value.copy()
426 428 human_value['used'] = format_byte_size_binary(value['used'])
427 429 human_value['total'] = format_byte_size_binary(value['total'])
428 human_value['text'] = "{} ({} items)".format(
429 human_value['used'], value['items'])
430 human_value['text'] = f"{human_value['used']} ({value['items']} items)"
431
432 return SysInfoRes(value=value, state=state, human_value=human_value)
433
434
435 @register_sysinfo
436 def storage_archives():
437 import rhodecode
438 from rhodecode.lib.helpers import format_byte_size_binary
439 import rhodecode.apps.file_store.utils as store_utils
440 from rhodecode import CONFIG
441
442 backend_type = rhodecode.ConfigGet().get_str(store_utils.config_keys.backend_type)
443
444 value = dict(percent=0, used=0, total=0, items=0, path='', text='', type=backend_type)
445 state = STATE_OK_DEFAULT
446 try:
447 f_store = store_utils.get_filestore_backend(config=CONFIG)
448 backend_type = str(f_store)
449 total_files, total_size, _directory_stats = f_store.get_statistics()
450
451 value.update({
452 'percent': 100,
453 'used': total_size,
454 'total': total_size,
455 'items': total_files,
456 'path': f_store.storage_path,
457 'type': backend_type
458 })
459
460 except Exception as e:
461 log.exception('failed to fetch archive cache storage')
462 state = {'message': str(e), 'type': STATE_ERR}
463
464 human_value = value.copy()
465 human_value['used'] = format_byte_size_binary(value['used'])
466 human_value['total'] = format_byte_size_binary(value['total'])
467 human_value['text'] = f"{human_value['used']} ({value['items']} items)"
430 468
431 469 return SysInfoRes(value=value, state=state, human_value=human_value)
432 470
433 471
434 472 @register_sysinfo
435 473 def storage_gist():
436 474 from rhodecode.model.gist import GIST_STORE_LOC
437 475 from rhodecode.lib.utils import safe_str, get_rhodecode_repo_store_path
438 476 from rhodecode.lib.helpers import format_byte_size_binary, get_directory_statistics
439 477
440 478 path = safe_str(os.path.join(
441 479 get_rhodecode_repo_store_path(), GIST_STORE_LOC))
442 480
443 481 # gist storage
444 482 value = dict(percent=0, used=0, total=0, items=0, path=path, text='')
445 483 state = STATE_OK_DEFAULT
446 484
447 485 try:
448 486 total_files, total_size, _directory_stats = get_directory_statistics(path)
449 487 value.update({
450 488 'percent': 100,
451 489 'used': total_size,
452 490 'total': total_size,
453 491 'items': total_files
454 492 })
455 493 except Exception as e:
456 494 log.exception('failed to fetch gist storage items')
457 495 state = {'message': str(e), 'type': STATE_ERR}
458 496
459 497 human_value = value.copy()
460 498 human_value['used'] = format_byte_size_binary(value['used'])
461 499 human_value['total'] = format_byte_size_binary(value['total'])
462 500 human_value['text'] = "{} ({} items)".format(
463 501 human_value['used'], value['items'])
464 502
465 503 return SysInfoRes(value=value, state=state, human_value=human_value)
466 504
467 505
468 506 @register_sysinfo
469 507 def storage_temp():
470 508 import tempfile
471 509 from rhodecode.lib.helpers import format_byte_size_binary
472 510
473 511 path = tempfile.gettempdir()
474 512 value = dict(percent=0, used=0, total=0, items=0, path=path, text='')
475 513 state = STATE_OK_DEFAULT
476 514
477 515 if not psutil:
478 516 return SysInfoRes(value=value, state=state)
479 517
480 518 try:
481 519 value.update(dict(psutil.disk_usage(path)._asdict()))
482 520 except Exception as e:
483 521 log.exception('Failed to fetch temp dir info')
484 522 state = {'message': str(e), 'type': STATE_ERR}
485 523
486 524 human_value = value.copy()
487 525 human_value['used'] = format_byte_size_binary(value['used'])
488 526 human_value['total'] = format_byte_size_binary(value['total'])
489 527 human_value['text'] = "{}/{}, {}% used".format(
490 528 format_byte_size_binary(value['used']),
491 529 format_byte_size_binary(value['total']),
492 530 value['percent'])
493 531
494 532 return SysInfoRes(value=value, state=state, human_value=human_value)
495 533
496 534
497 535 @register_sysinfo
498 536 def search_info():
499 537 import rhodecode
500 538 from rhodecode.lib.index import searcher_from_config
501 539
502 540 backend = rhodecode.CONFIG.get('search.module', '')
503 541 location = rhodecode.CONFIG.get('search.location', '')
504 542
505 543 try:
506 544 searcher = searcher_from_config(rhodecode.CONFIG)
507 545 searcher = searcher.__class__.__name__
508 546 except Exception:
509 547 searcher = None
510 548
511 549 value = dict(
512 550 backend=backend, searcher=searcher, location=location, text='')
513 551 state = STATE_OK_DEFAULT
514 552
515 553 human_value = value.copy()
516 554 human_value['text'] = "backend:`{}`".format(human_value['backend'])
517 555
518 556 return SysInfoRes(value=value, state=state, human_value=human_value)
519 557
520 558
521 559 @register_sysinfo
522 560 def git_info():
523 561 from rhodecode.lib.vcs.backends import git
524 562 state = STATE_OK_DEFAULT
525 563 value = human_value = ''
526 564 try:
527 565 value = git.discover_git_version(raise_on_exc=True)
528 566 human_value = f'version reported from VCSServer: {value}'
529 567 except Exception as e:
530 568 state = {'message': str(e), 'type': STATE_ERR}
531 569
532 570 return SysInfoRes(value=value, state=state, human_value=human_value)
533 571
534 572
535 573 @register_sysinfo
536 574 def hg_info():
537 575 from rhodecode.lib.vcs.backends import hg
538 576 state = STATE_OK_DEFAULT
539 577 value = human_value = ''
540 578 try:
541 579 value = hg.discover_hg_version(raise_on_exc=True)
542 580 human_value = f'version reported from VCSServer: {value}'
543 581 except Exception as e:
544 582 state = {'message': str(e), 'type': STATE_ERR}
545 583 return SysInfoRes(value=value, state=state, human_value=human_value)
546 584
547 585
548 586 @register_sysinfo
549 587 def svn_info():
550 588 from rhodecode.lib.vcs.backends import svn
551 589 state = STATE_OK_DEFAULT
552 590 value = human_value = ''
553 591 try:
554 592 value = svn.discover_svn_version(raise_on_exc=True)
555 593 human_value = f'version reported from VCSServer: {value}'
556 594 except Exception as e:
557 595 state = {'message': str(e), 'type': STATE_ERR}
558 596 return SysInfoRes(value=value, state=state, human_value=human_value)
559 597
560 598
561 599 @register_sysinfo
562 600 def vcs_backends():
563 601 import rhodecode
564 602 value = rhodecode.CONFIG.get('vcs.backends')
565 603 human_value = 'Enabled backends in order: {}'.format(','.join(value))
566 604 return SysInfoRes(value=value, human_value=human_value)
567 605
568 606
569 607 @register_sysinfo
570 608 def vcs_server():
571 609 import rhodecode
572 610 from rhodecode.lib.vcs.backends import get_vcsserver_service_data
573 611
574 612 server_url = rhodecode.CONFIG.get('vcs.server')
575 613 enabled = rhodecode.CONFIG.get('vcs.server.enable')
576 614 protocol = rhodecode.CONFIG.get('vcs.server.protocol') or 'http'
577 615 state = STATE_OK_DEFAULT
578 616 version = None
579 617 workers = 0
580 618
581 619 try:
582 620 data = get_vcsserver_service_data()
583 621 if data and 'version' in data:
584 622 version = data['version']
585 623
586 624 if data and 'config' in data:
587 625 conf = data['config']
588 626 workers = conf.get('workers', 'NOT AVAILABLE')
589 627
590 628 connection = 'connected'
591 629 except Exception as e:
592 630 connection = 'failed'
593 631 state = {'message': str(e), 'type': STATE_ERR}
594 632
595 633 value = dict(
596 634 url=server_url,
597 635 enabled=enabled,
598 636 protocol=protocol,
599 637 connection=connection,
600 638 version=version,
601 639 text='',
602 640 )
603 641
604 642 human_value = value.copy()
605 643 human_value['text'] = \
606 644 '{url}@ver:{ver} via {mode} mode[workers:{workers}], connection:{conn}'.format(
607 645 url=server_url, ver=version, workers=workers, mode=protocol,
608 646 conn=connection)
609 647
610 648 return SysInfoRes(value=value, state=state, human_value=human_value)
611 649
612 650
613 651 @register_sysinfo
614 652 def vcs_server_config():
615 653 from rhodecode.lib.vcs.backends import get_vcsserver_service_data
616 654 state = STATE_OK_DEFAULT
617 655
618 656 value = {}
619 657 try:
620 658 data = get_vcsserver_service_data()
621 659 value = data['app_config']
622 660 except Exception as e:
623 661 state = {'message': str(e), 'type': STATE_ERR}
624 662
625 663 human_value = value.copy()
626 664 human_value['text'] = 'VCS Server config'
627 665
628 666 return SysInfoRes(value=value, state=state, human_value=human_value)
629 667
630 668
631 669 @register_sysinfo
632 670 def rhodecode_app_info():
633 671 import rhodecode
634 672 edition = rhodecode.CONFIG.get('rhodecode.edition')
635 673
636 674 value = dict(
637 675 rhodecode_version=rhodecode.__version__,
638 676 rhodecode_lib_path=os.path.abspath(rhodecode.__file__),
639 677 text=''
640 678 )
641 679 human_value = value.copy()
642 680 human_value['text'] = 'RhodeCode {edition}, version {ver}'.format(
643 681 edition=edition, ver=value['rhodecode_version']
644 682 )
645 683 return SysInfoRes(value=value, human_value=human_value)
646 684
647 685
648 686 @register_sysinfo
649 687 def rhodecode_config():
650 688 import rhodecode
651 689 path = rhodecode.CONFIG.get('__file__')
652 690 rhodecode_ini_safe = rhodecode.CONFIG.copy()
653 691 cert_path = get_cert_path(path)
654 692
655 693 try:
656 694 config = configparser.ConfigParser()
657 695 config.read(path)
658 696 parsed_ini = config
659 697 if parsed_ini.has_section('server:main'):
660 698 parsed_ini = dict(parsed_ini.items('server:main'))
661 699 except Exception:
662 700 log.exception('Failed to read .ini file for display')
663 701 parsed_ini = {}
664 702
665 703 rhodecode_ini_safe['server:main'] = parsed_ini
666 704
667 705 blacklist = [
668 706 f'rhodecode_{LicenseModel.LICENSE_DB_KEY}',
669 707 'routes.map',
670 708 'sqlalchemy.db1.url',
671 709 'channelstream.secret',
672 710 'beaker.session.secret',
673 711 'rhodecode.encrypted_values.secret',
674 712 'rhodecode_auth_github_consumer_key',
675 713 'rhodecode_auth_github_consumer_secret',
676 714 'rhodecode_auth_google_consumer_key',
677 715 'rhodecode_auth_google_consumer_secret',
678 716 'rhodecode_auth_bitbucket_consumer_secret',
679 717 'rhodecode_auth_bitbucket_consumer_key',
680 718 'rhodecode_auth_twitter_consumer_secret',
681 719 'rhodecode_auth_twitter_consumer_key',
682 720
683 721 'rhodecode_auth_twitter_secret',
684 722 'rhodecode_auth_github_secret',
685 723 'rhodecode_auth_google_secret',
686 724 'rhodecode_auth_bitbucket_secret',
687 725
688 726 'appenlight.api_key',
689 727 ('app_conf', 'sqlalchemy.db1.url')
690 728 ]
691 729 for k in blacklist:
692 730 if isinstance(k, tuple):
693 731 section, key = k
694 732 if section in rhodecode_ini_safe:
695 733 rhodecode_ini_safe[section] = '**OBFUSCATED**'
696 734 else:
697 735 rhodecode_ini_safe.pop(k, None)
698 736
699 737 # TODO: maybe put some CONFIG checks here ?
700 738 return SysInfoRes(value={'config': rhodecode_ini_safe,
701 739 'path': path, 'cert_path': cert_path})
702 740
703 741
704 742 @register_sysinfo
705 743 def database_info():
706 744 import rhodecode
707 745 from sqlalchemy.engine import url as engine_url
708 746 from rhodecode.model import meta
709 747 from rhodecode.model.meta import Session
710 748 from rhodecode.model.db import DbMigrateVersion
711 749
712 750 state = STATE_OK_DEFAULT
713 751
714 752 db_migrate = DbMigrateVersion.query().filter(
715 753 DbMigrateVersion.repository_id == 'rhodecode_db_migrations').one()
716 754
717 755 db_url_obj = engine_url.make_url(rhodecode.CONFIG['sqlalchemy.db1.url'])
718 756
719 757 try:
720 758 engine = meta.get_engine()
721 759 db_server_info = engine.dialect._get_server_version_info(
722 760 Session.connection(bind=engine))
723 761 db_version = '.'.join(map(str, db_server_info))
724 762 except Exception:
725 763 log.exception('failed to fetch db version')
726 764 db_version = 'UNKNOWN'
727 765
728 766 db_info = dict(
729 767 migrate_version=db_migrate.version,
730 768 type=db_url_obj.get_backend_name(),
731 769 version=db_version,
732 770 url=repr(db_url_obj)
733 771 )
734 772 current_version = db_migrate.version
735 773 expected_version = rhodecode.__dbversion__
736 774 if state['type'] == STATE_OK and current_version != expected_version:
737 775 msg = 'Critical: database schema mismatch, ' \
738 776 'expected version {}, got {}. ' \
739 777 'Please run migrations on your database.'.format(
740 778 expected_version, current_version)
741 779 state = {'message': msg, 'type': STATE_ERR}
742 780
743 781 human_value = db_info.copy()
744 782 human_value['url'] = "{} @ migration version: {}".format(
745 783 db_info['url'], db_info['migrate_version'])
746 784 human_value['version'] = "{} {}".format(db_info['type'], db_info['version'])
747 785 return SysInfoRes(value=db_info, state=state, human_value=human_value)
748 786
749 787
750 788 @register_sysinfo
751 789 def server_info(environ):
752 790 import rhodecode
753 791 from rhodecode.lib.base import get_server_ip_addr, get_server_port
754 792
755 793 value = {
756 794 'server_ip': '{}:{}'.format(
757 795 get_server_ip_addr(environ, log_errors=False),
758 796 get_server_port(environ)
759 797 ),
760 798 'server_id': rhodecode.CONFIG.get('instance_id'),
761 799 }
762 800 return SysInfoRes(value=value)
763 801
764 802
765 803 @register_sysinfo
766 804 def usage_info():
767 805 from rhodecode.model.db import User, Repository, true
768 806 value = {
769 807 'users': User.query().count(),
770 808 'users_active': User.query().filter(User.active == true()).count(),
771 809 'repositories': Repository.query().count(),
772 810 'repository_types': {
773 811 'hg': Repository.query().filter(
774 812 Repository.repo_type == 'hg').count(),
775 813 'git': Repository.query().filter(
776 814 Repository.repo_type == 'git').count(),
777 815 'svn': Repository.query().filter(
778 816 Repository.repo_type == 'svn').count(),
779 817 },
780 818 }
781 819 return SysInfoRes(value=value)
782 820
783 821
784 822 def get_system_info(environ):
785 823 environ = environ or {}
786 824 return {
787 825 'rhodecode_app': SysInfo(rhodecode_app_info)(),
788 826 'rhodecode_config': SysInfo(rhodecode_config)(),
789 827 'rhodecode_usage': SysInfo(usage_info)(),
790 828 'python': SysInfo(python_info)(),
791 829 'py_modules': SysInfo(py_modules)(),
792 830
793 831 'platform': SysInfo(platform_type)(),
794 832 'locale': SysInfo(locale_info)(),
795 833 'server': SysInfo(server_info, environ=environ)(),
796 834 'database': SysInfo(database_info)(),
797 835 'ulimit': SysInfo(ulimit_info)(),
798 836 'storage': SysInfo(storage)(),
799 837 'storage_inodes': SysInfo(storage_inodes)(),
800 838 'storage_archive': SysInfo(storage_archives)(),
839 'storage_artifacts': SysInfo(storage_artifacts)(),
801 840 'storage_gist': SysInfo(storage_gist)(),
802 841 'storage_temp': SysInfo(storage_temp)(),
803 842
804 843 'search': SysInfo(search_info)(),
805 844
806 845 'uptime': SysInfo(uptime)(),
807 846 'load': SysInfo(machine_load)(),
808 847 'cpu': SysInfo(cpu)(),
809 848 'memory': SysInfo(memory)(),
810 849
811 850 'vcs_backends': SysInfo(vcs_backends)(),
812 851 'vcs_server': SysInfo(vcs_server)(),
813 852
814 853 'vcs_server_config': SysInfo(vcs_server_config)(),
815 854
816 855 'git': SysInfo(git_info)(),
817 856 'hg': SysInfo(hg_info)(),
818 857 'svn': SysInfo(svn_info)(),
819 858 }
820 859
821 860
822 861 def load_system_info(key):
823 862 """
824 863 get_sys_info('vcs_server')
825 864 get_sys_info('database')
826 865 """
827 866 return SysInfo(registered_helpers[key])()
@@ -1,824 +1,864 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 Utilities library for RhodeCode
21 21 """
22 22
23 23 import datetime
24 24
25 25 import decorator
26 26 import logging
27 27 import os
28 28 import re
29 29 import sys
30 30 import shutil
31 31 import socket
32 32 import tempfile
33 33 import traceback
34 34 import tarfile
35 35
36 36 from functools import wraps
37 37 from os.path import join as jn
38 38
39 39 import paste
40 40 import pkg_resources
41 41 from webhelpers2.text import collapse, strip_tags, convert_accented_entities, convert_misc_entities
42 42
43 43 from mako import exceptions
44 44
45 from rhodecode import ConfigGet
46 from rhodecode.lib.exceptions import HTTPBranchProtected, HTTPLockedRC
45 47 from rhodecode.lib.hash_utils import sha256_safe, md5, sha1
46 48 from rhodecode.lib.type_utils import AttributeDict
47 49 from rhodecode.lib.str_utils import safe_bytes, safe_str
48 50 from rhodecode.lib.vcs.backends.base import Config
49 51 from rhodecode.lib.vcs.exceptions import VCSError
50 52 from rhodecode.lib.vcs.utils.helpers import get_scm, get_scm_backend
51 53 from rhodecode.lib.ext_json import sjson as json
52 54 from rhodecode.model import meta
53 55 from rhodecode.model.db import (
54 56 Repository, User, RhodeCodeUi, UserLog, RepoGroup, UserGroup)
55 57 from rhodecode.model.meta import Session
56 58
57 59
58 60 log = logging.getLogger(__name__)
59 61
60 62 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
61 63
62 64 # String which contains characters that are not allowed in slug names for
63 65 # repositories or repository groups. It is properly escaped to use it in
64 66 # regular expressions.
65 67 SLUG_BAD_CHARS = re.escape(r'`?=[]\;\'"<>,/~!@#$%^&*()+{}|:')
66 68
67 69 # Regex that matches forbidden characters in repo/group slugs.
68 70 SLUG_BAD_CHAR_RE = re.compile(r'[{}\x00-\x08\x0b-\x0c\x0e-\x1f]'.format(SLUG_BAD_CHARS))
69 71
70 72 # Regex that matches allowed characters in repo/group slugs.
71 73 SLUG_GOOD_CHAR_RE = re.compile(r'[^{}]'.format(SLUG_BAD_CHARS))
72 74
73 75 # Regex that matches whole repo/group slugs.
74 76 SLUG_RE = re.compile(r'[^{}]+'.format(SLUG_BAD_CHARS))
75 77
76 78 _license_cache = None
77 79
78 80
79 81 def adopt_for_celery(func):
80 82 """
81 83 Decorator designed to adopt hooks (from rhodecode.lib.hooks_base)
82 84 for further usage as a celery tasks.
83 85 """
84 86 @wraps(func)
85 87 def wrapper(extras):
86 88 extras = AttributeDict(extras)
87 # HooksResponse implements to_json method which must be used there.
88 return func(extras).to_json()
89 try:
90 # HooksResponse implements to_json method which must be used there.
91 return func(extras).to_json()
92 except HTTPBranchProtected as error:
93 # Those special cases don't need error reporting. It's a case of
94 # locked repo or protected branch
95 error_args = error.args
96 return {
97 'status': error.code,
98 'output': error.explanation,
99 'exception': type(error).__name__,
100 'exception_args': error_args,
101 'exception_traceback': '',
102 }
103 except HTTPLockedRC as error:
104 # Those special cases don't need error reporting. It's a case of
105 # locked repo or protected branch
106 error_args = error.args
107 return {
108 'status': error.code,
109 'output': error.explanation,
110 'exception': type(error).__name__,
111 'exception_args': error_args,
112 'exception_traceback': '',
113 }
114 except Exception as e:
115 return {
116 'status': 128,
117 'output': '',
118 'exception': type(e).__name__,
119 'exception_args': e.args,
120 'exception_traceback': '',
121 }
89 122 return wrapper
90 123
91 124
92 125 def repo_name_slug(value):
93 126 """
94 127 Return slug of name of repository
95 128 This function is called on each creation/modification
96 129 of repository to prevent bad names in repo
97 130 """
98 131
99 132 replacement_char = '-'
100 133
101 134 slug = strip_tags(value)
102 135 slug = convert_accented_entities(slug)
103 136 slug = convert_misc_entities(slug)
104 137
105 138 slug = SLUG_BAD_CHAR_RE.sub('', slug)
106 139 slug = re.sub(r'[\s]+', '-', slug)
107 140 slug = collapse(slug, replacement_char)
108 141
109 142 return slug
110 143
111 144
112 145 #==============================================================================
113 146 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
114 147 #==============================================================================
115 148 def get_repo_slug(request):
116 149 _repo = ''
117 150
118 151 if hasattr(request, 'db_repo_name'):
119 152 # if our requests has set db reference use it for name, this
120 153 # translates the example.com/_<id> into proper repo names
121 154 _repo = request.db_repo_name
122 155 elif getattr(request, 'matchdict', None):
123 156 # pyramid
124 157 _repo = request.matchdict.get('repo_name')
125 158
126 159 if _repo:
127 160 _repo = _repo.rstrip('/')
128 161 return _repo
129 162
130 163
131 164 def get_repo_group_slug(request):
132 165 _group = ''
133 166 if hasattr(request, 'db_repo_group'):
134 167 # if our requests has set db reference use it for name, this
135 168 # translates the example.com/_<id> into proper repo group names
136 169 _group = request.db_repo_group.group_name
137 170 elif getattr(request, 'matchdict', None):
138 171 # pyramid
139 172 _group = request.matchdict.get('repo_group_name')
140 173
141 174 if _group:
142 175 _group = _group.rstrip('/')
143 176 return _group
144 177
145 178
146 179 def get_user_group_slug(request):
147 180 _user_group = ''
148 181
149 182 if hasattr(request, 'db_user_group'):
150 183 _user_group = request.db_user_group.users_group_name
151 184 elif getattr(request, 'matchdict', None):
152 185 # pyramid
153 186 _user_group = request.matchdict.get('user_group_id')
154 187 _user_group_name = request.matchdict.get('user_group_name')
155 188 try:
156 189 if _user_group:
157 190 _user_group = UserGroup.get(_user_group)
158 191 elif _user_group_name:
159 192 _user_group = UserGroup.get_by_group_name(_user_group_name)
160 193
161 194 if _user_group:
162 195 _user_group = _user_group.users_group_name
163 196 except Exception:
164 197 log.exception('Failed to get user group by id and name')
165 198 # catch all failures here
166 199 return None
167 200
168 201 return _user_group
169 202
170 203
171 204 def get_filesystem_repos(path, recursive=False, skip_removed_repos=True):
172 205 """
173 206 Scans given path for repos and return (name,(type,path)) tuple
174 207
175 208 :param path: path to scan for repositories
176 209 :param recursive: recursive search and return names with subdirs in front
177 210 """
178 211
179 212 # remove ending slash for better results
180 213 path = path.rstrip(os.sep)
181 214 log.debug('now scanning in %s location recursive:%s...', path, recursive)
182 215
183 216 def _get_repos(p):
184 217 dirpaths = get_dirpaths(p)
185 218 if not _is_dir_writable(p):
186 219 log.warning('repo path without write access: %s', p)
187 220
188 221 for dirpath in dirpaths:
189 222 if os.path.isfile(os.path.join(p, dirpath)):
190 223 continue
191 224 cur_path = os.path.join(p, dirpath)
192 225
193 226 # skip removed repos
194 227 if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
195 228 continue
196 229
197 230 #skip .<somethin> dirs
198 231 if dirpath.startswith('.'):
199 232 continue
200 233
201 234 try:
202 235 scm_info = get_scm(cur_path)
203 236 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
204 237 except VCSError:
205 238 if not recursive:
206 239 continue
207 240 #check if this dir containts other repos for recursive scan
208 241 rec_path = os.path.join(p, dirpath)
209 242 if os.path.isdir(rec_path):
210 243 yield from _get_repos(rec_path)
211 244
212 245 return _get_repos(path)
213 246
214 247
215 248 def get_dirpaths(p: str) -> list:
216 249 try:
217 250 # OS-independable way of checking if we have at least read-only
218 251 # access or not.
219 252 dirpaths = os.listdir(p)
220 253 except OSError:
221 254 log.warning('ignoring repo path without read access: %s', p)
222 255 return []
223 256
224 257 # os.listpath has a tweak: If a unicode is passed into it, then it tries to
225 258 # decode paths and suddenly returns unicode objects itself. The items it
226 259 # cannot decode are returned as strings and cause issues.
227 260 #
228 261 # Those paths are ignored here until a solid solution for path handling has
229 262 # been built.
230 263 expected_type = type(p)
231 264
232 265 def _has_correct_type(item):
233 266 if type(item) is not expected_type:
234 267 log.error(
235 268 "Ignoring path %s since it cannot be decoded into str.",
236 269 # Using "repr" to make sure that we see the byte value in case
237 270 # of support.
238 271 repr(item))
239 272 return False
240 273 return True
241 274
242 275 dirpaths = [item for item in dirpaths if _has_correct_type(item)]
243 276
244 277 return dirpaths
245 278
246 279
247 280 def _is_dir_writable(path):
248 281 """
249 282 Probe if `path` is writable.
250 283
251 284 Due to trouble on Cygwin / Windows, this is actually probing if it is
252 285 possible to create a file inside of `path`, stat does not produce reliable
253 286 results in this case.
254 287 """
255 288 try:
256 289 with tempfile.TemporaryFile(dir=path):
257 290 pass
258 291 except OSError:
259 292 return False
260 293 return True
261 294
262 295
263 296 def is_valid_repo(repo_name, base_path, expect_scm=None, explicit_scm=None, config=None):
264 297 """
265 298 Returns True if given path is a valid repository False otherwise.
266 299 If expect_scm param is given also, compare if given scm is the same
267 300 as expected from scm parameter. If explicit_scm is given don't try to
268 301 detect the scm, just use the given one to check if repo is valid
269 302
270 303 :param repo_name:
271 304 :param base_path:
272 305 :param expect_scm:
273 306 :param explicit_scm:
274 307 :param config:
275 308
276 309 :return True: if given path is a valid repository
277 310 """
278 311 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
279 312 log.debug('Checking if `%s` is a valid path for repository. '
280 313 'Explicit type: %s', repo_name, explicit_scm)
281 314
282 315 try:
283 316 if explicit_scm:
284 317 detected_scms = [get_scm_backend(explicit_scm)(
285 318 full_path, config=config).alias]
286 319 else:
287 320 detected_scms = get_scm(full_path)
288 321
289 322 if expect_scm:
290 323 return detected_scms[0] == expect_scm
291 324 log.debug('path: %s is an vcs object:%s', full_path, detected_scms)
292 325 return True
293 326 except VCSError:
294 327 log.debug('path: %s is not a valid repo !', full_path)
295 328 return False
296 329
297 330
298 331 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
299 332 """
300 333 Returns True if a given path is a repository group, False otherwise
301 334
302 335 :param repo_group_name:
303 336 :param base_path:
304 337 """
305 338 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
306 339 log.debug('Checking if `%s` is a valid path for repository group',
307 340 repo_group_name)
308 341
309 342 # check if it's not a repo
310 343 if is_valid_repo(repo_group_name, base_path):
311 344 log.debug('Repo called %s exist, it is not a valid repo group', repo_group_name)
312 345 return False
313 346
314 347 try:
315 348 # we need to check bare git repos at higher level
316 349 # since we might match branches/hooks/info/objects or possible
317 350 # other things inside bare git repo
318 351 maybe_repo = os.path.dirname(full_path)
319 352 if maybe_repo == base_path:
320 353 # skip root level repo check; we know root location CANNOT BE a repo group
321 354 return False
322 355
323 356 scm_ = get_scm(maybe_repo)
324 357 log.debug('path: %s is a vcs object:%s, not valid repo group', full_path, scm_)
325 358 return False
326 359 except VCSError:
327 360 pass
328 361
329 362 # check if it's a valid path
330 363 if skip_path_check or os.path.isdir(full_path):
331 364 log.debug('path: %s is a valid repo group !', full_path)
332 365 return True
333 366
334 367 log.debug('path: %s is not a valid repo group !', full_path)
335 368 return False
336 369
337 370
338 371 def ask_ok(prompt, retries=4, complaint='[y]es or [n]o please!'):
339 372 while True:
340 373 ok = input(prompt)
341 374 if ok.lower() in ('y', 'ye', 'yes'):
342 375 return True
343 376 if ok.lower() in ('n', 'no', 'nop', 'nope'):
344 377 return False
345 378 retries = retries - 1
346 379 if retries < 0:
347 380 raise OSError
348 381 print(complaint)
349 382
350 383 # propagated from mercurial documentation
351 384 ui_sections = [
352 385 'alias', 'auth',
353 386 'decode/encode', 'defaults',
354 387 'diff', 'email',
355 388 'extensions', 'format',
356 389 'merge-patterns', 'merge-tools',
357 390 'hooks', 'http_proxy',
358 391 'smtp', 'patch',
359 392 'paths', 'profiling',
360 393 'server', 'trusted',
361 394 'ui', 'web', ]
362 395
363 396
364 def config_data_from_db(clear_session=True, repo=None):
397 def prepare_config_data(clear_session=True, repo=None):
365 398 """
366 Read the configuration data from the database and return configuration
399 Read the configuration data from the database, *.ini files and return configuration
367 400 tuples.
368 401 """
369 402 from rhodecode.model.settings import VcsSettingsModel
370 403
371 config = []
372
373 404 sa = meta.Session()
374 405 settings_model = VcsSettingsModel(repo=repo, sa=sa)
375 406
376 407 ui_settings = settings_model.get_ui_settings()
377 408
378 409 ui_data = []
410 config = [
411 ('web', 'push_ssl', 'false'),
412 ]
379 413 for setting in ui_settings:
414 # Todo: remove this section once transition to *.ini files will be completed
415 if setting.section in ('largefiles', 'vcs_git_lfs'):
416 if setting.key != 'enabled':
417 continue
380 418 if setting.active:
381 419 ui_data.append((setting.section, setting.key, setting.value))
382 420 config.append((
383 421 safe_str(setting.section), safe_str(setting.key),
384 422 safe_str(setting.value)))
385 423 if setting.key == 'push_ssl':
386 # force set push_ssl requirement to False, rhodecode
387 # handles that
424 # force set push_ssl requirement to False this is deprecated, and we must force it to False
388 425 config.append((
389 426 safe_str(setting.section), safe_str(setting.key), False))
427 config_getter = ConfigGet()
428 config.append(('vcs_git_lfs', 'store_location', config_getter.get_str('vcs.git.lfs.storage_location')))
429 config.append(('largefiles', 'usercache', config_getter.get_str('vcs.hg.largefiles.storage_location')))
390 430 log.debug(
391 431 'settings ui from db@repo[%s]: %s',
392 432 repo,
393 433 ','.join(['[{}] {}={}'.format(*s) for s in ui_data]))
394 434 if clear_session:
395 435 meta.Session.remove()
396 436
397 437 # TODO: mikhail: probably it makes no sense to re-read hooks information.
398 438 # It's already there and activated/deactivated
399 439 skip_entries = []
400 440 enabled_hook_classes = get_enabled_hook_classes(ui_settings)
401 441 if 'pull' not in enabled_hook_classes:
402 442 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PULL))
403 443 if 'push' not in enabled_hook_classes:
404 444 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PUSH))
405 445 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRETX_PUSH))
406 446 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PUSH_KEY))
407 447
408 448 config = [entry for entry in config if entry[:2] not in skip_entries]
409 449
410 450 return config
411 451
412 452
413 453 def make_db_config(clear_session=True, repo=None):
414 454 """
415 455 Create a :class:`Config` instance based on the values in the database.
416 456 """
417 457 config = Config()
418 config_data = config_data_from_db(clear_session=clear_session, repo=repo)
458 config_data = prepare_config_data(clear_session=clear_session, repo=repo)
419 459 for section, option, value in config_data:
420 460 config.set(section, option, value)
421 461 return config
422 462
423 463
424 464 def get_enabled_hook_classes(ui_settings):
425 465 """
426 466 Return the enabled hook classes.
427 467
428 468 :param ui_settings: List of ui_settings as returned
429 469 by :meth:`VcsSettingsModel.get_ui_settings`
430 470
431 471 :return: a list with the enabled hook classes. The order is not guaranteed.
432 472 :rtype: list
433 473 """
434 474 enabled_hooks = []
435 475 active_hook_keys = [
436 476 key for section, key, value, active in ui_settings
437 477 if section == 'hooks' and active]
438 478
439 479 hook_names = {
440 480 RhodeCodeUi.HOOK_PUSH: 'push',
441 481 RhodeCodeUi.HOOK_PULL: 'pull',
442 482 RhodeCodeUi.HOOK_REPO_SIZE: 'repo_size'
443 483 }
444 484
445 485 for key in active_hook_keys:
446 486 hook = hook_names.get(key)
447 487 if hook:
448 488 enabled_hooks.append(hook)
449 489
450 490 return enabled_hooks
451 491
452 492
453 493 def set_rhodecode_config(config):
454 494 """
455 495 Updates pyramid config with new settings from database
456 496
457 497 :param config:
458 498 """
459 499 from rhodecode.model.settings import SettingsModel
460 500 app_settings = SettingsModel().get_all_settings()
461 501
462 502 for k, v in list(app_settings.items()):
463 503 config[k] = v
464 504
465 505
466 506 def get_rhodecode_realm():
467 507 """
468 508 Return the rhodecode realm from database.
469 509 """
470 510 from rhodecode.model.settings import SettingsModel
471 511 realm = SettingsModel().get_setting_by_name('realm')
472 512 return safe_str(realm.app_settings_value)
473 513
474 514
475 515 def get_rhodecode_repo_store_path():
476 516 """
477 517 Returns the base path. The base path is the filesystem path which points
478 518 to the repository store.
479 519 """
480 520
481 521 import rhodecode
482 522 return rhodecode.CONFIG['repo_store.path']
483 523
484 524
485 525 def map_groups(path):
486 526 """
487 527 Given a full path to a repository, create all nested groups that this
488 528 repo is inside. This function creates parent-child relationships between
489 529 groups and creates default perms for all new groups.
490 530
491 531 :param paths: full path to repository
492 532 """
493 533 from rhodecode.model.repo_group import RepoGroupModel
494 534 sa = meta.Session()
495 535 groups = path.split(Repository.NAME_SEP)
496 536 parent = None
497 537 group = None
498 538
499 539 # last element is repo in nested groups structure
500 540 groups = groups[:-1]
501 541 rgm = RepoGroupModel(sa)
502 542 owner = User.get_first_super_admin()
503 543 for lvl, group_name in enumerate(groups):
504 544 group_name = '/'.join(groups[:lvl] + [group_name])
505 545 group = RepoGroup.get_by_group_name(group_name)
506 546 desc = '%s group' % group_name
507 547
508 548 # skip folders that are now removed repos
509 549 if REMOVED_REPO_PAT.match(group_name):
510 550 break
511 551
512 552 if group is None:
513 553 log.debug('creating group level: %s group_name: %s',
514 554 lvl, group_name)
515 555 group = RepoGroup(group_name, parent)
516 556 group.group_description = desc
517 557 group.user = owner
518 558 sa.add(group)
519 559 perm_obj = rgm._create_default_perms(group)
520 560 sa.add(perm_obj)
521 561 sa.flush()
522 562
523 563 parent = group
524 564 return group
525 565
526 566
527 567 def repo2db_mapper(initial_repo_list, remove_obsolete=False, force_hooks_rebuild=False):
528 568 """
529 569 maps all repos given in initial_repo_list, non existing repositories
530 570 are created, if remove_obsolete is True it also checks for db entries
531 571 that are not in initial_repo_list and removes them.
532 572
533 573 :param initial_repo_list: list of repositories found by scanning methods
534 574 :param remove_obsolete: check for obsolete entries in database
535 575 """
536 576 from rhodecode.model.repo import RepoModel
537 577 from rhodecode.model.repo_group import RepoGroupModel
538 578 from rhodecode.model.settings import SettingsModel
539 579
540 580 sa = meta.Session()
541 581 repo_model = RepoModel()
542 582 user = User.get_first_super_admin()
543 583 added = []
544 584
545 585 # creation defaults
546 586 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
547 587 enable_statistics = defs.get('repo_enable_statistics')
548 588 enable_locking = defs.get('repo_enable_locking')
549 589 enable_downloads = defs.get('repo_enable_downloads')
550 590 private = defs.get('repo_private')
551 591
552 592 for name, repo in list(initial_repo_list.items()):
553 593 group = map_groups(name)
554 594 str_name = safe_str(name)
555 595 db_repo = repo_model.get_by_repo_name(str_name)
556 596
557 597 # found repo that is on filesystem not in RhodeCode database
558 598 if not db_repo:
559 599 log.info('repository `%s` not found in the database, creating now', name)
560 600 added.append(name)
561 601 desc = (repo.description
562 602 if repo.description != 'unknown'
563 603 else '%s repository' % name)
564 604
565 605 db_repo = repo_model._create_repo(
566 606 repo_name=name,
567 607 repo_type=repo.alias,
568 608 description=desc,
569 609 repo_group=getattr(group, 'group_id', None),
570 610 owner=user,
571 611 enable_locking=enable_locking,
572 612 enable_downloads=enable_downloads,
573 613 enable_statistics=enable_statistics,
574 614 private=private,
575 615 state=Repository.STATE_CREATED
576 616 )
577 617 sa.commit()
578 618 # we added that repo just now, and make sure we updated server info
579 619 if db_repo.repo_type == 'git':
580 620 git_repo = db_repo.scm_instance()
581 621 # update repository server-info
582 622 log.debug('Running update server info')
583 623 git_repo._update_server_info(force=True)
584 624
585 db_repo.update_commit_cache()
625 db_repo.update_commit_cache(recursive=False)
586 626
587 627 config = db_repo._config
588 628 config.set('extensions', 'largefiles', '')
589 629 repo = db_repo.scm_instance(config=config)
590 630 repo.install_hooks(force=force_hooks_rebuild)
591 631
592 632 removed = []
593 633 if remove_obsolete:
594 634 # remove from database those repositories that are not in the filesystem
595 635 for repo in sa.query(Repository).all():
596 636 if repo.repo_name not in list(initial_repo_list.keys()):
597 637 log.debug("Removing non-existing repository found in db `%s`",
598 638 repo.repo_name)
599 639 try:
600 640 RepoModel(sa).delete(repo, forks='detach', fs_remove=False)
601 641 sa.commit()
602 642 removed.append(repo.repo_name)
603 643 except Exception:
604 644 # don't hold further removals on error
605 645 log.error(traceback.format_exc())
606 646 sa.rollback()
607 647
608 648 def splitter(full_repo_name):
609 649 _parts = full_repo_name.rsplit(RepoGroup.url_sep(), 1)
610 650 gr_name = None
611 651 if len(_parts) == 2:
612 652 gr_name = _parts[0]
613 653 return gr_name
614 654
615 655 initial_repo_group_list = [splitter(x) for x in
616 656 list(initial_repo_list.keys()) if splitter(x)]
617 657
618 658 # remove from database those repository groups that are not in the
619 659 # filesystem due to parent child relationships we need to delete them
620 660 # in a specific order of most nested first
621 661 all_groups = [x.group_name for x in sa.query(RepoGroup).all()]
622 662 def nested_sort(gr):
623 663 return len(gr.split('/'))
624 664 for group_name in sorted(all_groups, key=nested_sort, reverse=True):
625 665 if group_name not in initial_repo_group_list:
626 666 repo_group = RepoGroup.get_by_group_name(group_name)
627 667 if (repo_group.children.all() or
628 668 not RepoGroupModel().check_exist_filesystem(
629 669 group_name=group_name, exc_on_failure=False)):
630 670 continue
631 671
632 672 log.info(
633 673 'Removing non-existing repository group found in db `%s`',
634 674 group_name)
635 675 try:
636 676 RepoGroupModel(sa).delete(group_name, fs_remove=False)
637 677 sa.commit()
638 678 removed.append(group_name)
639 679 except Exception:
640 680 # don't hold further removals on error
641 681 log.exception(
642 682 'Unable to remove repository group `%s`',
643 683 group_name)
644 684 sa.rollback()
645 685 raise
646 686
647 687 return added, removed
648 688
649 689
650 690 def load_rcextensions(root_path):
651 691 import rhodecode
652 692 from rhodecode.config import conf
653 693
654 694 path = os.path.join(root_path)
655 695 sys.path.append(path)
656 696
657 697 try:
658 698 rcextensions = __import__('rcextensions')
659 699 except ImportError:
660 700 if os.path.isdir(os.path.join(path, 'rcextensions')):
661 701 log.warning('Unable to load rcextensions from %s', path)
662 702 rcextensions = None
663 703
664 704 if rcextensions:
665 705 log.info('Loaded rcextensions from %s...', rcextensions)
666 706 rhodecode.EXTENSIONS = rcextensions
667 707
668 708 # Additional mappings that are not present in the pygments lexers
669 709 conf.LANGUAGES_EXTENSIONS_MAP.update(
670 710 getattr(rhodecode.EXTENSIONS, 'EXTRA_MAPPINGS', {}))
671 711
672 712
673 713 def get_custom_lexer(extension):
674 714 """
675 715 returns a custom lexer if it is defined in rcextensions module, or None
676 716 if there's no custom lexer defined
677 717 """
678 718 import rhodecode
679 719 from pygments import lexers
680 720
681 721 # custom override made by RhodeCode
682 722 if extension in ['mako']:
683 723 return lexers.get_lexer_by_name('html+mako')
684 724
685 725 # check if we didn't define this extension as other lexer
686 726 extensions = rhodecode.EXTENSIONS and getattr(rhodecode.EXTENSIONS, 'EXTRA_LEXERS', None)
687 727 if extensions and extension in rhodecode.EXTENSIONS.EXTRA_LEXERS:
688 728 _lexer_name = rhodecode.EXTENSIONS.EXTRA_LEXERS[extension]
689 729 return lexers.get_lexer_by_name(_lexer_name)
690 730
691 731
692 732 #==============================================================================
693 733 # TEST FUNCTIONS AND CREATORS
694 734 #==============================================================================
695 735 def create_test_index(repo_location, config):
696 736 """
697 737 Makes default test index.
698 738 """
699 739 try:
700 740 import rc_testdata
701 741 except ImportError:
702 742 raise ImportError('Failed to import rc_testdata, '
703 743 'please make sure this package is installed from requirements_test.txt')
704 744 rc_testdata.extract_search_index(
705 745 'vcs_search_index', os.path.dirname(config['search.location']))
706 746
707 747
708 748 def create_test_directory(test_path):
709 749 """
710 750 Create test directory if it doesn't exist.
711 751 """
712 752 if not os.path.isdir(test_path):
713 753 log.debug('Creating testdir %s', test_path)
714 754 os.makedirs(test_path)
715 755
716 756
717 757 def create_test_database(test_path, config):
718 758 """
719 759 Makes a fresh database.
720 760 """
721 761 from rhodecode.lib.db_manage import DbManage
722 762 from rhodecode.lib.utils2 import get_encryption_key
723 763
724 764 # PART ONE create db
725 765 dbconf = config['sqlalchemy.db1.url']
726 766 enc_key = get_encryption_key(config)
727 767
728 768 log.debug('making test db %s', dbconf)
729 769
730 770 dbmanage = DbManage(log_sql=False, dbconf=dbconf, root=config['here'],
731 771 tests=True, cli_args={'force_ask': True}, enc_key=enc_key)
732 772 dbmanage.create_tables(override=True)
733 773 dbmanage.set_db_version()
734 774 # for tests dynamically set new root paths based on generated content
735 775 dbmanage.create_settings(dbmanage.config_prompt(test_path))
736 776 dbmanage.create_default_user()
737 777 dbmanage.create_test_admin_and_users()
738 778 dbmanage.create_permissions()
739 779 dbmanage.populate_default_permissions()
740 780 Session().commit()
741 781
742 782
743 783 def create_test_repositories(test_path, config):
744 784 """
745 785 Creates test repositories in the temporary directory. Repositories are
746 786 extracted from archives within the rc_testdata package.
747 787 """
748 788 import rc_testdata
749 789 from rhodecode.tests import HG_REPO, GIT_REPO, SVN_REPO
750 790
751 791 log.debug('making test vcs repositories')
752 792
753 793 idx_path = config['search.location']
754 794 data_path = config['cache_dir']
755 795
756 796 # clean index and data
757 797 if idx_path and os.path.exists(idx_path):
758 798 log.debug('remove %s', idx_path)
759 799 shutil.rmtree(idx_path)
760 800
761 801 if data_path and os.path.exists(data_path):
762 802 log.debug('remove %s', data_path)
763 803 shutil.rmtree(data_path)
764 804
765 805 rc_testdata.extract_hg_dump('vcs_test_hg', jn(test_path, HG_REPO))
766 806 rc_testdata.extract_git_dump('vcs_test_git', jn(test_path, GIT_REPO))
767 807
768 808 # Note: Subversion is in the process of being integrated with the system,
769 809 # until we have a properly packed version of the test svn repository, this
770 810 # tries to copy over the repo from a package "rc_testdata"
771 811 svn_repo_path = rc_testdata.get_svn_repo_archive()
772 812 with tarfile.open(svn_repo_path) as tar:
773 813 tar.extractall(jn(test_path, SVN_REPO))
774 814
775 815
776 816 def password_changed(auth_user, session):
777 817 # Never report password change in case of default user or anonymous user.
778 818 if auth_user.username == User.DEFAULT_USER or auth_user.user_id is None:
779 819 return False
780 820
781 821 password_hash = md5(safe_bytes(auth_user.password)) if auth_user.password else None
782 822 rhodecode_user = session.get('rhodecode_user', {})
783 823 session_password_hash = rhodecode_user.get('password', '')
784 824 return password_hash != session_password_hash
785 825
786 826
787 827 def read_opensource_licenses():
788 828 global _license_cache
789 829
790 830 if not _license_cache:
791 831 licenses = pkg_resources.resource_string(
792 832 'rhodecode', 'config/licenses.json')
793 833 _license_cache = json.loads(licenses)
794 834
795 835 return _license_cache
796 836
797 837
798 838 def generate_platform_uuid():
799 839 """
800 840 Generates platform UUID based on it's name
801 841 """
802 842 import platform
803 843
804 844 try:
805 845 uuid_list = [platform.platform()]
806 846 return sha256_safe(':'.join(uuid_list))
807 847 except Exception as e:
808 848 log.error('Failed to generate host uuid: %s', e)
809 849 return 'UNDEFINED'
810 850
811 851
812 852 def send_test_email(recipients, email_body='TEST EMAIL'):
813 853 """
814 854 Simple code for generating test emails.
815 855 Usage::
816 856
817 857 from rhodecode.lib import utils
818 858 utils.send_test_email()
819 859 """
820 860 from rhodecode.lib.celerylib import tasks, run_task
821 861
822 862 email_body = email_body_plaintext = email_body
823 863 subject = f'SUBJECT FROM: {socket.gethostname()}'
824 864 tasks.send_email(recipients, subject, email_body_plaintext, email_body)
@@ -1,6046 +1,6055 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 Database Models for RhodeCode Enterprise
21 21 """
22 22
23 23 import re
24 24 import os
25 25 import time
26 26 import string
27 27 import logging
28 28 import datetime
29 29 import uuid
30 30 import warnings
31 31 import ipaddress
32 32 import functools
33 33 import traceback
34 34 import collections
35 35
36 36 import pyotp
37 37 from sqlalchemy import (
38 38 or_, and_, not_, func, cast, TypeDecorator, event, select,
39 39 true, false, null, union_all,
40 40 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
41 41 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
42 42 Text, Float, PickleType, BigInteger)
43 43 from sqlalchemy.sql.expression import case
44 44 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
45 45 from sqlalchemy.orm import (
46 46 relationship, lazyload, joinedload, class_mapper, validates, aliased, load_only)
47 47 from sqlalchemy.ext.declarative import declared_attr
48 48 from sqlalchemy.ext.hybrid import hybrid_property
49 49 from sqlalchemy.exc import IntegrityError # pragma: no cover
50 50 from sqlalchemy.dialects.mysql import LONGTEXT
51 51 from zope.cachedescriptors.property import Lazy as LazyProperty
52 52 from pyramid.threadlocal import get_current_request
53 53 from webhelpers2.text import remove_formatting
54 54
55 55 from rhodecode import ConfigGet
56 56 from rhodecode.lib.str_utils import safe_bytes
57 57 from rhodecode.translation import _
58 58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
59 59 from rhodecode.lib.vcs.backends.base import (
60 60 EmptyCommit, Reference, unicode_to_reference, reference_to_unicode)
61 61 from rhodecode.lib.utils2 import (
62 62 str2bool, safe_str, get_commit_safe, sha1_safe,
63 63 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
64 64 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time)
65 65 from rhodecode.lib.jsonalchemy import (
66 66 MutationObj, MutationList, JsonType, JsonRaw)
67 67 from rhodecode.lib.hash_utils import sha1
68 68 from rhodecode.lib import ext_json
69 69 from rhodecode.lib import enc_utils
70 70 from rhodecode.lib.ext_json import json, str_json
71 71 from rhodecode.lib.caching_query import FromCache
72 72 from rhodecode.lib.exceptions import (
73 73 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
74 74 from rhodecode.model.meta import Base, Session
75 75
76 76 URL_SEP = '/'
77 77 log = logging.getLogger(__name__)
78 78
79 79 # =============================================================================
80 80 # BASE CLASSES
81 81 # =============================================================================
82 82
83 83 # this is propagated from .ini file rhodecode.encrypted_values.secret or
84 84 # beaker.session.secret if first is not set.
85 85 # and initialized at environment.py
86 86 ENCRYPTION_KEY: bytes = b''
87 87
88 88 # used to sort permissions by types, '#' used here is not allowed to be in
89 89 # usernames, and it's very early in sorted string.printable table.
90 90 PERMISSION_TYPE_SORT = {
91 91 'admin': '####',
92 92 'write': '###',
93 93 'read': '##',
94 94 'none': '#',
95 95 }
96 96
97 97
98 98 def display_user_sort(obj):
99 99 """
100 100 Sort function used to sort permissions in .permissions() function of
101 101 Repository, RepoGroup, UserGroup. Also it put the default user in front
102 102 of all other resources
103 103 """
104 104
105 105 if obj.username == User.DEFAULT_USER:
106 106 return '#####'
107 107 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
108 108 extra_sort_num = '1' # default
109 109
110 110 # NOTE(dan): inactive duplicates goes last
111 111 if getattr(obj, 'duplicate_perm', None):
112 112 extra_sort_num = '9'
113 113 return prefix + extra_sort_num + obj.username
114 114
115 115
116 116 def display_user_group_sort(obj):
117 117 """
118 118 Sort function used to sort permissions in .permissions() function of
119 119 Repository, RepoGroup, UserGroup. Also it put the default user in front
120 120 of all other resources
121 121 """
122 122
123 123 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
124 124 return prefix + obj.users_group_name
125 125
126 126
127 127 def _hash_key(k):
128 128 return sha1_safe(k)
129 129
130 130
131 131 def description_escaper(desc):
132 132 from rhodecode.lib import helpers as h
133 133 return h.escape(desc)
134 134
135 135
136 136 def in_filter_generator(qry, items, limit=500):
137 137 """
138 138 Splits IN() into multiple with OR
139 139 e.g.::
140 140 cnt = Repository.query().filter(
141 141 or_(
142 142 *in_filter_generator(Repository.repo_id, range(100000))
143 143 )).count()
144 144 """
145 145 if not items:
146 146 # empty list will cause empty query which might cause security issues
147 147 # this can lead to hidden unpleasant results
148 148 items = [-1]
149 149
150 150 parts = []
151 151 for chunk in range(0, len(items), limit):
152 152 parts.append(
153 153 qry.in_(items[chunk: chunk + limit])
154 154 )
155 155
156 156 return parts
157 157
158 158
159 159 base_table_args = {
160 160 'extend_existing': True,
161 161 'mysql_engine': 'InnoDB',
162 162 'mysql_charset': 'utf8',
163 163 'sqlite_autoincrement': True
164 164 }
165 165
166 166
167 167 class EncryptedTextValue(TypeDecorator):
168 168 """
169 169 Special column for encrypted long text data, use like::
170 170
171 171 value = Column("encrypted_value", EncryptedValue(), nullable=False)
172 172
173 173 This column is intelligent so if value is in unencrypted form it return
174 174 unencrypted form, but on save it always encrypts
175 175 """
176 176 cache_ok = True
177 177 impl = Text
178 178
179 179 def process_bind_param(self, value, dialect):
180 180 """
181 181 Setter for storing value
182 182 """
183 183 import rhodecode
184 184 if not value:
185 185 return value
186 186
187 187 # protect against double encrypting if values is already encrypted
188 188 if value.startswith('enc$aes$') \
189 189 or value.startswith('enc$aes_hmac$') \
190 190 or value.startswith('enc2$'):
191 191 raise ValueError('value needs to be in unencrypted format, '
192 192 'ie. not starting with enc$ or enc2$')
193 193
194 194 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
195 195 bytes_val = enc_utils.encrypt_value(value, enc_key=ENCRYPTION_KEY, algo=algo)
196 196 return safe_str(bytes_val)
197 197
198 198 def process_result_value(self, value, dialect):
199 199 """
200 200 Getter for retrieving value
201 201 """
202 202
203 203 import rhodecode
204 204 if not value:
205 205 return value
206 206
207 207 bytes_val = enc_utils.decrypt_value(value, enc_key=ENCRYPTION_KEY)
208 208
209 209 return safe_str(bytes_val)
210 210
211 211
212 212 class BaseModel(object):
213 213 """
214 214 Base Model for all classes
215 215 """
216 216
217 217 @classmethod
218 218 def _get_keys(cls):
219 219 """return column names for this model """
220 220 return class_mapper(cls).c.keys()
221 221
222 222 def get_dict(self):
223 223 """
224 224 return dict with keys and values corresponding
225 225 to this model data """
226 226
227 227 d = {}
228 228 for k in self._get_keys():
229 229 d[k] = getattr(self, k)
230 230
231 231 # also use __json__() if present to get additional fields
232 232 _json_attr = getattr(self, '__json__', None)
233 233 if _json_attr:
234 234 # update with attributes from __json__
235 235 if callable(_json_attr):
236 236 _json_attr = _json_attr()
237 237 for k, val in _json_attr.items():
238 238 d[k] = val
239 239 return d
240 240
241 241 def get_appstruct(self):
242 242 """return list with keys and values tuples corresponding
243 243 to this model data """
244 244
245 245 lst = []
246 246 for k in self._get_keys():
247 247 lst.append((k, getattr(self, k),))
248 248 return lst
249 249
250 250 def populate_obj(self, populate_dict):
251 251 """populate model with data from given populate_dict"""
252 252
253 253 for k in self._get_keys():
254 254 if k in populate_dict:
255 255 setattr(self, k, populate_dict[k])
256 256
257 257 @classmethod
258 258 def query(cls):
259 259 return Session().query(cls)
260 260
261 261 @classmethod
262 262 def select(cls, custom_cls=None):
263 263 """
264 264 stmt = cls.select().where(cls.user_id==1)
265 265 # optionally
266 266 stmt = cls.select(User.user_id).where(cls.user_id==1)
267 267 result = cls.execute(stmt) | cls.scalars(stmt)
268 268 """
269 269
270 270 if custom_cls:
271 271 stmt = select(custom_cls)
272 272 else:
273 273 stmt = select(cls)
274 274 return stmt
275 275
276 276 @classmethod
277 277 def execute(cls, stmt):
278 278 return Session().execute(stmt)
279 279
280 280 @classmethod
281 281 def scalars(cls, stmt):
282 282 return Session().scalars(stmt)
283 283
284 284 @classmethod
285 285 def get(cls, id_):
286 286 if id_:
287 287 return cls.query().get(id_)
288 288
289 289 @classmethod
290 290 def get_or_404(cls, id_):
291 291 from pyramid.httpexceptions import HTTPNotFound
292 292
293 293 try:
294 294 id_ = int(id_)
295 295 except (TypeError, ValueError):
296 296 raise HTTPNotFound()
297 297
298 298 res = cls.query().get(id_)
299 299 if not res:
300 300 raise HTTPNotFound()
301 301 return res
302 302
303 303 @classmethod
304 304 def getAll(cls):
305 305 # deprecated and left for backward compatibility
306 306 return cls.get_all()
307 307
308 308 @classmethod
309 309 def get_all(cls):
310 310 return cls.query().all()
311 311
312 312 @classmethod
313 313 def delete(cls, id_):
314 314 obj = cls.query().get(id_)
315 315 Session().delete(obj)
316 316
317 317 @classmethod
318 318 def identity_cache(cls, session, attr_name, value):
319 319 exist_in_session = []
320 320 for (item_cls, pkey), instance in session.identity_map.items():
321 321 if cls == item_cls and getattr(instance, attr_name) == value:
322 322 exist_in_session.append(instance)
323 323 if exist_in_session:
324 324 if len(exist_in_session) == 1:
325 325 return exist_in_session[0]
326 326 log.exception(
327 327 'multiple objects with attr %s and '
328 328 'value %s found with same name: %r',
329 329 attr_name, value, exist_in_session)
330 330
331 331 @property
332 332 def cls_name(self):
333 333 return self.__class__.__name__
334 334
335 335 def __repr__(self):
336 336 return f'<DB:{self.cls_name}>'
337 337
338 338
339 339 class RhodeCodeSetting(Base, BaseModel):
340 340 __tablename__ = 'rhodecode_settings'
341 341 __table_args__ = (
342 342 UniqueConstraint('app_settings_name'),
343 343 base_table_args
344 344 )
345 345
346 346 SETTINGS_TYPES = {
347 347 'str': safe_str,
348 348 'int': safe_int,
349 349 'unicode': safe_str,
350 350 'bool': str2bool,
351 351 'list': functools.partial(aslist, sep=',')
352 352 }
353 353 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
354 354 GLOBAL_CONF_KEY = 'app_settings'
355 355
356 356 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
357 357 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
358 358 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
359 359 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
360 360
361 361 def __init__(self, key='', val='', type='unicode'):
362 362 self.app_settings_name = key
363 363 self.app_settings_type = type
364 364 self.app_settings_value = val
365 365
366 366 @validates('_app_settings_value')
367 367 def validate_settings_value(self, key, val):
368 368 assert type(val) == str
369 369 return val
370 370
371 371 @hybrid_property
372 372 def app_settings_value(self):
373 373 v = self._app_settings_value
374 374 _type = self.app_settings_type
375 375 if _type:
376 376 _type = self.app_settings_type.split('.')[0]
377 377 # decode the encrypted value
378 378 if 'encrypted' in self.app_settings_type:
379 379 cipher = EncryptedTextValue()
380 380 v = safe_str(cipher.process_result_value(v, None))
381 381
382 382 converter = self.SETTINGS_TYPES.get(_type) or \
383 383 self.SETTINGS_TYPES['unicode']
384 384 return converter(v)
385 385
386 386 @app_settings_value.setter
387 387 def app_settings_value(self, val):
388 388 """
389 389 Setter that will always make sure we use unicode in app_settings_value
390 390
391 391 :param val:
392 392 """
393 393 val = safe_str(val)
394 394 # encode the encrypted value
395 395 if 'encrypted' in self.app_settings_type:
396 396 cipher = EncryptedTextValue()
397 397 val = safe_str(cipher.process_bind_param(val, None))
398 398 self._app_settings_value = val
399 399
400 400 @hybrid_property
401 401 def app_settings_type(self):
402 402 return self._app_settings_type
403 403
404 404 @app_settings_type.setter
405 405 def app_settings_type(self, val):
406 406 if val.split('.')[0] not in self.SETTINGS_TYPES:
407 407 raise Exception('type must be one of %s got %s'
408 408 % (self.SETTINGS_TYPES.keys(), val))
409 409 self._app_settings_type = val
410 410
411 411 @classmethod
412 412 def get_by_prefix(cls, prefix):
413 413 return RhodeCodeSetting.query()\
414 414 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
415 415 .all()
416 416
417 417 def __repr__(self):
418 418 return "<%s('%s:%s[%s]')>" % (
419 419 self.cls_name,
420 420 self.app_settings_name, self.app_settings_value,
421 421 self.app_settings_type
422 422 )
423 423
424 424
425 425 class RhodeCodeUi(Base, BaseModel):
426 426 __tablename__ = 'rhodecode_ui'
427 427 __table_args__ = (
428 428 UniqueConstraint('ui_key'),
429 429 base_table_args
430 430 )
431 431 # Sync those values with vcsserver.config.hooks
432 432
433 433 HOOK_REPO_SIZE = 'changegroup.repo_size'
434 434 # HG
435 435 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
436 436 HOOK_PULL = 'outgoing.pull_logger'
437 437 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
438 438 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
439 439 HOOK_PUSH = 'changegroup.push_logger'
440 440 HOOK_PUSH_KEY = 'pushkey.key_push'
441 441
442 442 HOOKS_BUILTIN = [
443 443 HOOK_PRE_PULL,
444 444 HOOK_PULL,
445 445 HOOK_PRE_PUSH,
446 446 HOOK_PRETX_PUSH,
447 447 HOOK_PUSH,
448 448 HOOK_PUSH_KEY,
449 449 ]
450 450
451 451 # TODO: johbo: Unify way how hooks are configured for git and hg,
452 452 # git part is currently hardcoded.
453 453
454 454 # SVN PATTERNS
455 455 SVN_BRANCH_ID = 'vcs_svn_branch'
456 456 SVN_TAG_ID = 'vcs_svn_tag'
457 457
458 458 ui_id = Column(
459 459 "ui_id", Integer(), nullable=False, unique=True, default=None,
460 460 primary_key=True)
461 461 ui_section = Column(
462 462 "ui_section", String(255), nullable=True, unique=None, default=None)
463 463 ui_key = Column(
464 464 "ui_key", String(255), nullable=True, unique=None, default=None)
465 465 ui_value = Column(
466 466 "ui_value", String(255), nullable=True, unique=None, default=None)
467 467 ui_active = Column(
468 468 "ui_active", Boolean(), nullable=True, unique=None, default=True)
469 469
470 470 def __repr__(self):
471 471 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.ui_section,
472 472 self.ui_key, self.ui_value)
473 473
474 474
475 475 class RepoRhodeCodeSetting(Base, BaseModel):
476 476 __tablename__ = 'repo_rhodecode_settings'
477 477 __table_args__ = (
478 478 UniqueConstraint(
479 479 'app_settings_name', 'repository_id',
480 480 name='uq_repo_rhodecode_setting_name_repo_id'),
481 481 base_table_args
482 482 )
483 483
484 484 repository_id = Column(
485 485 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
486 486 nullable=False)
487 487 app_settings_id = Column(
488 488 "app_settings_id", Integer(), nullable=False, unique=True,
489 489 default=None, primary_key=True)
490 490 app_settings_name = Column(
491 491 "app_settings_name", String(255), nullable=True, unique=None,
492 492 default=None)
493 493 _app_settings_value = Column(
494 494 "app_settings_value", String(4096), nullable=True, unique=None,
495 495 default=None)
496 496 _app_settings_type = Column(
497 497 "app_settings_type", String(255), nullable=True, unique=None,
498 498 default=None)
499 499
500 500 repository = relationship('Repository', viewonly=True)
501 501
502 502 def __init__(self, repository_id, key='', val='', type='unicode'):
503 503 self.repository_id = repository_id
504 504 self.app_settings_name = key
505 505 self.app_settings_type = type
506 506 self.app_settings_value = val
507 507
508 508 @validates('_app_settings_value')
509 509 def validate_settings_value(self, key, val):
510 510 assert type(val) == str
511 511 return val
512 512
513 513 @hybrid_property
514 514 def app_settings_value(self):
515 515 v = self._app_settings_value
516 516 type_ = self.app_settings_type
517 517 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
518 518 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
519 519 return converter(v)
520 520
521 521 @app_settings_value.setter
522 522 def app_settings_value(self, val):
523 523 """
524 524 Setter that will always make sure we use unicode in app_settings_value
525 525
526 526 :param val:
527 527 """
528 528 self._app_settings_value = safe_str(val)
529 529
530 530 @hybrid_property
531 531 def app_settings_type(self):
532 532 return self._app_settings_type
533 533
534 534 @app_settings_type.setter
535 535 def app_settings_type(self, val):
536 536 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
537 537 if val not in SETTINGS_TYPES:
538 538 raise Exception('type must be one of %s got %s'
539 539 % (SETTINGS_TYPES.keys(), val))
540 540 self._app_settings_type = val
541 541
542 542 def __repr__(self):
543 543 return "<%s('%s:%s:%s[%s]')>" % (
544 544 self.cls_name, self.repository.repo_name,
545 545 self.app_settings_name, self.app_settings_value,
546 546 self.app_settings_type
547 547 )
548 548
549 549
550 550 class RepoRhodeCodeUi(Base, BaseModel):
551 551 __tablename__ = 'repo_rhodecode_ui'
552 552 __table_args__ = (
553 553 UniqueConstraint(
554 554 'repository_id', 'ui_section', 'ui_key',
555 555 name='uq_repo_rhodecode_ui_repository_id_section_key'),
556 556 base_table_args
557 557 )
558 558
559 559 repository_id = Column(
560 560 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
561 561 nullable=False)
562 562 ui_id = Column(
563 563 "ui_id", Integer(), nullable=False, unique=True, default=None,
564 564 primary_key=True)
565 565 ui_section = Column(
566 566 "ui_section", String(255), nullable=True, unique=None, default=None)
567 567 ui_key = Column(
568 568 "ui_key", String(255), nullable=True, unique=None, default=None)
569 569 ui_value = Column(
570 570 "ui_value", String(255), nullable=True, unique=None, default=None)
571 571 ui_active = Column(
572 572 "ui_active", Boolean(), nullable=True, unique=None, default=True)
573 573
574 574 repository = relationship('Repository', viewonly=True)
575 575
576 576 def __repr__(self):
577 577 return '<%s[%s:%s]%s=>%s]>' % (
578 578 self.cls_name, self.repository.repo_name,
579 579 self.ui_section, self.ui_key, self.ui_value)
580 580
581 581
582 582 class User(Base, BaseModel):
583 583 __tablename__ = 'users'
584 584 __table_args__ = (
585 585 UniqueConstraint('username'), UniqueConstraint('email'),
586 586 Index('u_username_idx', 'username'),
587 587 Index('u_email_idx', 'email'),
588 588 base_table_args
589 589 )
590 590
591 591 DEFAULT_USER = 'default'
592 592 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
593 593 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
594 594 RECOVERY_CODES_COUNT = 10
595 595
596 596 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
597 597 username = Column("username", String(255), nullable=True, unique=None, default=None)
598 598 password = Column("password", String(255), nullable=True, unique=None, default=None)
599 599 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
600 600 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
601 601 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
602 602 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
603 603 _email = Column("email", String(255), nullable=True, unique=None, default=None)
604 604 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
605 605 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
606 606 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
607 607
608 608 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
609 609 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
610 610 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
611 611 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
612 612 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
613 613 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
614 614
615 615 user_log = relationship('UserLog', back_populates='user')
616 616 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
617 617
618 618 repositories = relationship('Repository', back_populates='user')
619 619 repository_groups = relationship('RepoGroup', back_populates='user')
620 620 user_groups = relationship('UserGroup', back_populates='user')
621 621
622 622 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all', back_populates='follows_user')
623 623 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all', back_populates='user')
624 624
625 625 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
626 626 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
627 627 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
628 628
629 629 group_member = relationship('UserGroupMember', cascade='all', back_populates='user')
630 630
631 631 notifications = relationship('UserNotification', cascade='all', back_populates='user')
632 632 # notifications assigned to this user
633 633 user_created_notifications = relationship('Notification', cascade='all', back_populates='created_by_user')
634 634 # comments created by this user
635 635 user_comments = relationship('ChangesetComment', cascade='all', back_populates='author')
636 636 # user profile extra info
637 637 user_emails = relationship('UserEmailMap', cascade='all', back_populates='user')
638 638 user_ip_map = relationship('UserIpMap', cascade='all', back_populates='user')
639 639 user_auth_tokens = relationship('UserApiKeys', cascade='all', back_populates='user')
640 640 user_ssh_keys = relationship('UserSshKeys', cascade='all', back_populates='user')
641 641
642 642 # gists
643 643 user_gists = relationship('Gist', cascade='all', back_populates='owner')
644 644 # user pull requests
645 645 user_pull_requests = relationship('PullRequest', cascade='all', back_populates='author')
646 646
647 647 # external identities
648 648 external_identities = relationship('ExternalIdentity', primaryjoin="User.user_id==ExternalIdentity.local_user_id", cascade='all')
649 649 # review rules
650 650 user_review_rules = relationship('RepoReviewRuleUser', cascade='all', back_populates='user')
651 651
652 652 # artifacts owned
653 653 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id', back_populates='upload_user')
654 654
655 655 # no cascade, set NULL
656 656 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id', cascade='', back_populates='user')
657 657
658 658 def __repr__(self):
659 659 return f"<{self.cls_name}('id={self.user_id}, username={self.username}')>"
660 660
661 661 @hybrid_property
662 662 def email(self):
663 663 return self._email
664 664
665 665 @email.setter
666 666 def email(self, val):
667 667 self._email = val.lower() if val else None
668 668
669 669 @hybrid_property
670 670 def first_name(self):
671 671 if self.name:
672 672 return description_escaper(self.name)
673 673 return self.name
674 674
675 675 @hybrid_property
676 676 def last_name(self):
677 677 if self.lastname:
678 678 return description_escaper(self.lastname)
679 679 return self.lastname
680 680
681 681 @hybrid_property
682 682 def api_key(self):
683 683 """
684 684 Fetch if exist an auth-token with role ALL connected to this user
685 685 """
686 686 user_auth_token = UserApiKeys.query()\
687 687 .filter(UserApiKeys.user_id == self.user_id)\
688 688 .filter(or_(UserApiKeys.expires == -1,
689 689 UserApiKeys.expires >= time.time()))\
690 690 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
691 691 if user_auth_token:
692 692 user_auth_token = user_auth_token.api_key
693 693
694 694 return user_auth_token
695 695
696 696 @api_key.setter
697 697 def api_key(self, val):
698 698 # don't allow to set API key this is deprecated for now
699 699 self._api_key = None
700 700
701 701 @property
702 702 def reviewer_pull_requests(self):
703 703 return PullRequestReviewers.query() \
704 704 .options(joinedload(PullRequestReviewers.pull_request)) \
705 705 .filter(PullRequestReviewers.user_id == self.user_id) \
706 706 .all()
707 707
708 708 @property
709 709 def firstname(self):
710 710 # alias for future
711 711 return self.name
712 712
713 713 @property
714 714 def emails(self):
715 715 other = UserEmailMap.query()\
716 716 .filter(UserEmailMap.user == self) \
717 717 .order_by(UserEmailMap.email_id.asc()) \
718 718 .all()
719 719 return [self.email] + [x.email for x in other]
720 720
721 721 def emails_cached(self):
722 722 emails = []
723 723 if self.user_id != self.get_default_user_id():
724 724 emails = UserEmailMap.query()\
725 725 .filter(UserEmailMap.user == self) \
726 726 .order_by(UserEmailMap.email_id.asc())
727 727
728 728 emails = emails.options(
729 729 FromCache("sql_cache_short", f"get_user_{self.user_id}_emails")
730 730 )
731 731
732 732 return [self.email] + [x.email for x in emails]
733 733
734 734 @property
735 735 def auth_tokens(self):
736 736 auth_tokens = self.get_auth_tokens()
737 737 return [x.api_key for x in auth_tokens]
738 738
739 739 def get_auth_tokens(self):
740 740 return UserApiKeys.query()\
741 741 .filter(UserApiKeys.user == self)\
742 742 .order_by(UserApiKeys.user_api_key_id.asc())\
743 743 .all()
744 744
745 745 @LazyProperty
746 746 def feed_token(self):
747 747 return self.get_feed_token()
748 748
749 749 def get_feed_token(self, cache=True):
750 750 feed_tokens = UserApiKeys.query()\
751 751 .filter(UserApiKeys.user == self)\
752 752 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
753 753 if cache:
754 754 feed_tokens = feed_tokens.options(
755 755 FromCache("sql_cache_short", f"get_user_feed_token_{self.user_id}"))
756 756
757 757 feed_tokens = feed_tokens.all()
758 758 if feed_tokens:
759 759 return feed_tokens[0].api_key
760 760 return 'NO_FEED_TOKEN_AVAILABLE'
761 761
762 762 @LazyProperty
763 763 def artifact_token(self):
764 764 return self.get_artifact_token()
765 765
766 766 def get_artifact_token(self, cache=True):
767 767 artifacts_tokens = UserApiKeys.query()\
768 768 .filter(UserApiKeys.user == self) \
769 769 .filter(or_(UserApiKeys.expires == -1,
770 770 UserApiKeys.expires >= time.time())) \
771 771 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
772 772
773 773 if cache:
774 774 artifacts_tokens = artifacts_tokens.options(
775 775 FromCache("sql_cache_short", f"get_user_artifact_token_{self.user_id}"))
776 776
777 777 artifacts_tokens = artifacts_tokens.all()
778 778 if artifacts_tokens:
779 779 return artifacts_tokens[0].api_key
780 780 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
781 781
782 782 def get_or_create_artifact_token(self):
783 783 artifacts_tokens = UserApiKeys.query()\
784 784 .filter(UserApiKeys.user == self) \
785 785 .filter(or_(UserApiKeys.expires == -1,
786 786 UserApiKeys.expires >= time.time())) \
787 787 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
788 788
789 789 artifacts_tokens = artifacts_tokens.all()
790 790 if artifacts_tokens:
791 791 return artifacts_tokens[0].api_key
792 792 else:
793 793 from rhodecode.model.auth_token import AuthTokenModel
794 794 artifact_token = AuthTokenModel().create(
795 795 self, 'auto-generated-artifact-token',
796 796 lifetime=-1, role=UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
797 797 Session.commit()
798 798 return artifact_token.api_key
799 799
800 800 def is_totp_valid(self, received_code, secret):
801 801 totp = pyotp.TOTP(secret)
802 802 return totp.verify(received_code)
803 803
804 804 def is_2fa_recovery_code_valid(self, received_code, secret):
805 805 encrypted_recovery_codes = self.user_data.get('recovery_codes_2fa', [])
806 806 recovery_codes = self.get_2fa_recovery_codes()
807 807 if received_code in recovery_codes:
808 808 encrypted_recovery_codes.pop(recovery_codes.index(received_code))
809 809 self.update_userdata(recovery_codes_2fa=encrypted_recovery_codes)
810 810 return True
811 811 return False
812 812
813 813 @hybrid_property
814 814 def has_forced_2fa(self):
815 815 """
816 816 Checks if 2fa was forced for current user
817 817 """
818 818 from rhodecode.model.settings import SettingsModel
819 819 if value := SettingsModel().get_setting_by_name(f'auth_{self.extern_type}_global_2fa'):
820 820 return value.app_settings_value
821 821 return False
822 822
823 823 @hybrid_property
824 824 def has_enabled_2fa(self):
825 825 """
826 826 Checks if user enabled 2fa
827 827 """
828 828 if value := self.has_forced_2fa:
829 829 return value
830 830 return self.user_data.get('enabled_2fa', False)
831 831
832 832 @has_enabled_2fa.setter
833 833 def has_enabled_2fa(self, val):
834 834 val = str2bool(val)
835 835 self.update_userdata(enabled_2fa=val)
836 836 if not val:
837 837 # NOTE: setting to false we clear the user_data to not store any 2fa artifacts
838 838 self.update_userdata(secret_2fa=None, recovery_codes_2fa=[], check_2fa=False)
839 839 Session().commit()
840 840
841 841 @hybrid_property
842 842 def check_2fa_required(self):
843 843 """
844 844 Check if check 2fa flag is set for this user
845 845 """
846 846 value = self.user_data.get('check_2fa', False)
847 847 return value
848 848
849 849 @check_2fa_required.setter
850 850 def check_2fa_required(self, val):
851 851 val = str2bool(val)
852 852 self.update_userdata(check_2fa=val)
853 853 Session().commit()
854 854
855 855 @hybrid_property
856 856 def has_seen_2fa_codes(self):
857 857 """
858 858 get the flag about if user has seen 2fa recovery codes
859 859 """
860 860 value = self.user_data.get('recovery_codes_2fa_seen', False)
861 861 return value
862 862
863 863 @has_seen_2fa_codes.setter
864 864 def has_seen_2fa_codes(self, val):
865 865 val = str2bool(val)
866 866 self.update_userdata(recovery_codes_2fa_seen=val)
867 867 Session().commit()
868 868
869 869 @hybrid_property
870 870 def needs_2fa_configure(self):
871 871 """
872 872 Determines if setup2fa has completed for this user. Means he has all needed data for 2fa to work.
873 873
874 874 Currently this is 2fa enabled and secret exists
875 875 """
876 876 if self.has_enabled_2fa:
877 877 return not self.user_data.get('secret_2fa')
878 878 return False
879 879
880 880 def init_2fa_recovery_codes(self, persist=True, force=False):
881 881 """
882 882 Creates 2fa recovery codes
883 883 """
884 884 recovery_codes = self.user_data.get('recovery_codes_2fa', [])
885 885 encrypted_codes = []
886 886 if not recovery_codes or force:
887 887 for _ in range(self.RECOVERY_CODES_COUNT):
888 888 recovery_code = pyotp.random_base32()
889 889 recovery_codes.append(recovery_code)
890 890 encrypted_code = enc_utils.encrypt_value(safe_bytes(recovery_code), enc_key=ENCRYPTION_KEY)
891 891 encrypted_codes.append(safe_str(encrypted_code))
892 892 if persist:
893 893 self.update_userdata(recovery_codes_2fa=encrypted_codes, recovery_codes_2fa_seen=False)
894 894 return recovery_codes
895 895 # User should not check the same recovery codes more than once
896 896 return []
897 897
898 898 def get_2fa_recovery_codes(self):
899 899 encrypted_recovery_codes = self.user_data.get('recovery_codes_2fa', [])
900 900
901 901 recovery_codes = list(map(
902 902 lambda val: safe_str(
903 903 enc_utils.decrypt_value(
904 904 val,
905 905 enc_key=ENCRYPTION_KEY
906 906 )),
907 907 encrypted_recovery_codes))
908 908 return recovery_codes
909 909
910 910 def init_secret_2fa(self, persist=True, force=False):
911 911 secret_2fa = self.user_data.get('secret_2fa')
912 912 if not secret_2fa or force:
913 913 secret = pyotp.random_base32()
914 914 if persist:
915 915 self.update_userdata(secret_2fa=safe_str(enc_utils.encrypt_value(safe_bytes(secret), enc_key=ENCRYPTION_KEY)))
916 916 return secret
917 917 return ''
918 918
919 919 @hybrid_property
920 920 def secret_2fa(self) -> str:
921 921 """
922 922 get stored secret for 2fa
923 923 """
924 924 secret_2fa = self.user_data.get('secret_2fa')
925 925 if secret_2fa:
926 926 return safe_str(
927 927 enc_utils.decrypt_value(secret_2fa, enc_key=ENCRYPTION_KEY))
928 928 return ''
929 929
930 930 @secret_2fa.setter
931 931 def secret_2fa(self, value: str) -> None:
932 932 encrypted_value = enc_utils.encrypt_value(safe_bytes(value), enc_key=ENCRYPTION_KEY)
933 933 self.update_userdata(secret_2fa=safe_str(encrypted_value))
934 934
935 935 def regenerate_2fa_recovery_codes(self):
936 936 """
937 937 Regenerates 2fa recovery codes upon request
938 938 """
939 939 new_recovery_codes = self.init_2fa_recovery_codes(force=True)
940 940 Session().commit()
941 941 return new_recovery_codes
942 942
943 943 @classmethod
944 944 def extra_valid_auth_tokens(cls, user, role=None):
945 945 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
946 946 .filter(or_(UserApiKeys.expires == -1,
947 947 UserApiKeys.expires >= time.time()))
948 948 if role:
949 949 tokens = tokens.filter(or_(UserApiKeys.role == role,
950 950 UserApiKeys.role == UserApiKeys.ROLE_ALL))
951 951 return tokens.all()
952 952
953 953 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
954 954 from rhodecode.lib import auth
955 955
956 956 log.debug('Trying to authenticate user: %s via auth-token, '
957 957 'and roles: %s', self, roles)
958 958
959 959 if not auth_token:
960 960 return False
961 961
962 962 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
963 963 tokens_q = UserApiKeys.query()\
964 964 .filter(UserApiKeys.user_id == self.user_id)\
965 965 .filter(or_(UserApiKeys.expires == -1,
966 966 UserApiKeys.expires >= time.time()))
967 967
968 968 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
969 969
970 970 crypto_backend = auth.crypto_backend()
971 971 enc_token_map = {}
972 972 plain_token_map = {}
973 973 for token in tokens_q:
974 974 if token.api_key.startswith(crypto_backend.ENC_PREF):
975 975 enc_token_map[token.api_key] = token
976 976 else:
977 977 plain_token_map[token.api_key] = token
978 978 log.debug(
979 979 'Found %s plain and %s encrypted tokens to check for authentication for this user',
980 980 len(plain_token_map), len(enc_token_map))
981 981
982 982 # plain token match comes first
983 983 match = plain_token_map.get(auth_token)
984 984
985 985 # check encrypted tokens now
986 986 if not match:
987 987 for token_hash, token in enc_token_map.items():
988 988 # NOTE(marcink): this is expensive to calculate, but most secure
989 989 if crypto_backend.hash_check(auth_token, token_hash):
990 990 match = token
991 991 break
992 992
993 993 if match:
994 994 log.debug('Found matching token %s', match)
995 995 if match.repo_id:
996 996 log.debug('Found scope, checking for scope match of token %s', match)
997 997 if match.repo_id == scope_repo_id:
998 998 return True
999 999 else:
1000 1000 log.debug(
1001 1001 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
1002 1002 'and calling scope is:%s, skipping further checks',
1003 1003 match.repo, scope_repo_id)
1004 1004 return False
1005 1005 else:
1006 1006 return True
1007 1007
1008 1008 return False
1009 1009
1010 1010 @property
1011 1011 def ip_addresses(self):
1012 1012 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
1013 1013 return [x.ip_addr for x in ret]
1014 1014
1015 1015 @property
1016 1016 def username_and_name(self):
1017 1017 return f'{self.username} ({self.first_name} {self.last_name})'
1018 1018
1019 1019 @property
1020 1020 def username_or_name_or_email(self):
1021 1021 full_name = self.full_name if self.full_name != ' ' else None
1022 1022 return self.username or full_name or self.email
1023 1023
1024 1024 @property
1025 1025 def full_name(self):
1026 1026 return f'{self.first_name} {self.last_name}'
1027 1027
1028 1028 @property
1029 1029 def full_name_or_username(self):
1030 1030 return (f'{self.first_name} {self.last_name}'
1031 1031 if (self.first_name and self.last_name) else self.username)
1032 1032
1033 1033 @property
1034 1034 def full_contact(self):
1035 1035 return f'{self.first_name} {self.last_name} <{self.email}>'
1036 1036
1037 1037 @property
1038 1038 def short_contact(self):
1039 1039 return f'{self.first_name} {self.last_name}'
1040 1040
1041 1041 @property
1042 1042 def is_admin(self):
1043 1043 return self.admin
1044 1044
1045 1045 @property
1046 1046 def language(self):
1047 1047 return self.user_data.get('language')
1048 1048
1049 1049 def AuthUser(self, **kwargs):
1050 1050 """
1051 1051 Returns instance of AuthUser for this user
1052 1052 """
1053 1053 from rhodecode.lib.auth import AuthUser
1054 1054 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
1055 1055
1056 1056 @hybrid_property
1057 1057 def user_data(self):
1058 1058 if not self._user_data:
1059 1059 return {}
1060 1060
1061 1061 try:
1062 1062 return json.loads(self._user_data) or {}
1063 1063 except TypeError:
1064 1064 return {}
1065 1065
1066 1066 @user_data.setter
1067 1067 def user_data(self, val):
1068 1068 if not isinstance(val, dict):
1069 1069 raise Exception(f'user_data must be dict, got {type(val)}')
1070 1070 try:
1071 1071 self._user_data = safe_bytes(json.dumps(val))
1072 1072 except Exception:
1073 1073 log.error(traceback.format_exc())
1074 1074
1075 1075 @classmethod
1076 1076 def get(cls, user_id, cache=False):
1077 1077 if not user_id:
1078 1078 return
1079 1079
1080 1080 user = cls.query()
1081 1081 if cache:
1082 1082 user = user.options(
1083 1083 FromCache("sql_cache_short", f"get_users_{user_id}"))
1084 1084 return user.get(user_id)
1085 1085
1086 1086 @classmethod
1087 1087 def get_by_username(cls, username, case_insensitive=False,
1088 1088 cache=False):
1089 1089
1090 1090 if case_insensitive:
1091 1091 q = cls.select().where(
1092 1092 func.lower(cls.username) == func.lower(username))
1093 1093 else:
1094 1094 q = cls.select().where(cls.username == username)
1095 1095
1096 1096 if cache:
1097 1097 hash_key = _hash_key(username)
1098 1098 q = q.options(
1099 1099 FromCache("sql_cache_short", f"get_user_by_name_{hash_key}"))
1100 1100
1101 1101 return cls.execute(q).scalar_one_or_none()
1102 1102
1103 1103 @classmethod
1104 1104 def get_by_username_or_primary_email(cls, user_identifier):
1105 1105 qs = union_all(cls.select().where(func.lower(cls.username) == func.lower(user_identifier)),
1106 1106 cls.select().where(func.lower(cls.email) == func.lower(user_identifier)))
1107 1107 return cls.execute(cls.select(User).from_statement(qs)).scalar_one_or_none()
1108 1108
1109 1109 @classmethod
1110 1110 def get_by_auth_token(cls, auth_token, cache=False):
1111 1111
1112 1112 q = cls.select(User)\
1113 1113 .join(UserApiKeys)\
1114 1114 .where(UserApiKeys.api_key == auth_token)\
1115 1115 .where(or_(UserApiKeys.expires == -1,
1116 1116 UserApiKeys.expires >= time.time()))
1117 1117
1118 1118 if cache:
1119 1119 q = q.options(
1120 1120 FromCache("sql_cache_short", f"get_auth_token_{auth_token}"))
1121 1121
1122 1122 matched_user = cls.execute(q).scalar_one_or_none()
1123 1123
1124 1124 return matched_user
1125 1125
1126 1126 @classmethod
1127 1127 def get_by_email(cls, email, case_insensitive=False, cache=False):
1128 1128
1129 1129 if case_insensitive:
1130 1130 q = cls.select().where(func.lower(cls.email) == func.lower(email))
1131 1131 else:
1132 1132 q = cls.select().where(cls.email == email)
1133 1133
1134 1134 if cache:
1135 1135 email_key = _hash_key(email)
1136 1136 q = q.options(
1137 1137 FromCache("sql_cache_short", f"get_email_key_{email_key}"))
1138 1138
1139 1139 ret = cls.execute(q).scalar_one_or_none()
1140 1140
1141 1141 if ret is None:
1142 1142 q = cls.select(UserEmailMap)
1143 1143 # try fetching in alternate email map
1144 1144 if case_insensitive:
1145 1145 q = q.where(func.lower(UserEmailMap.email) == func.lower(email))
1146 1146 else:
1147 1147 q = q.where(UserEmailMap.email == email)
1148 1148 q = q.options(joinedload(UserEmailMap.user))
1149 1149 if cache:
1150 1150 q = q.options(
1151 1151 FromCache("sql_cache_short", f"get_email_map_key_{email_key}"))
1152 1152
1153 1153 result = cls.execute(q).scalar_one_or_none()
1154 1154 ret = getattr(result, 'user', None)
1155 1155
1156 1156 return ret
1157 1157
1158 1158 @classmethod
1159 1159 def get_from_cs_author(cls, author):
1160 1160 """
1161 1161 Tries to get User objects out of commit author string
1162 1162
1163 1163 :param author:
1164 1164 """
1165 1165 from rhodecode.lib.helpers import email, author_name
1166 1166 # Valid email in the attribute passed, see if they're in the system
1167 1167 _email = email(author)
1168 1168 if _email:
1169 1169 user = cls.get_by_email(_email, case_insensitive=True)
1170 1170 if user:
1171 1171 return user
1172 1172 # Maybe we can match by username?
1173 1173 _author = author_name(author)
1174 1174 user = cls.get_by_username(_author, case_insensitive=True)
1175 1175 if user:
1176 1176 return user
1177 1177
1178 1178 def update_userdata(self, **kwargs):
1179 1179 usr = self
1180 1180 old = usr.user_data
1181 1181 old.update(**kwargs)
1182 1182 usr.user_data = old
1183 1183 Session().add(usr)
1184 1184 log.debug('updated userdata with %s', kwargs)
1185 1185
1186 1186 def update_lastlogin(self):
1187 1187 """Update user lastlogin"""
1188 1188 self.last_login = datetime.datetime.now()
1189 1189 Session().add(self)
1190 1190 log.debug('updated user %s lastlogin', self.username)
1191 1191
1192 1192 def update_password(self, new_password):
1193 1193 from rhodecode.lib.auth import get_crypt_password
1194 1194
1195 1195 self.password = get_crypt_password(new_password)
1196 1196 Session().add(self)
1197 1197
1198 1198 @classmethod
1199 1199 def get_first_super_admin(cls):
1200 1200 stmt = cls.select().where(User.admin == true()).order_by(User.user_id.asc())
1201 1201 user = cls.scalars(stmt).first()
1202 1202
1203 1203 if user is None:
1204 1204 raise Exception('FATAL: Missing administrative account!')
1205 1205 return user
1206 1206
1207 1207 @classmethod
1208 1208 def get_all_super_admins(cls, only_active=False):
1209 1209 """
1210 1210 Returns all admin accounts sorted by username
1211 1211 """
1212 1212 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1213 1213 if only_active:
1214 1214 qry = qry.filter(User.active == true())
1215 1215 return qry.all()
1216 1216
1217 1217 @classmethod
1218 1218 def get_all_user_ids(cls, only_active=True):
1219 1219 """
1220 1220 Returns all users IDs
1221 1221 """
1222 1222 qry = Session().query(User.user_id)
1223 1223
1224 1224 if only_active:
1225 1225 qry = qry.filter(User.active == true())
1226 1226 return [x.user_id for x in qry]
1227 1227
1228 1228 @classmethod
1229 1229 def get_default_user(cls, cache=False, refresh=False):
1230 1230 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1231 1231 if user is None:
1232 1232 raise Exception('FATAL: Missing default account!')
1233 1233 if refresh:
1234 1234 # The default user might be based on outdated state which
1235 1235 # has been loaded from the cache.
1236 1236 # A call to refresh() ensures that the
1237 1237 # latest state from the database is used.
1238 1238 Session().refresh(user)
1239 1239
1240 1240 return user
1241 1241
1242 1242 @classmethod
1243 1243 def get_default_user_id(cls):
1244 1244 import rhodecode
1245 1245 return rhodecode.CONFIG['default_user_id']
1246 1246
1247 1247 def _get_default_perms(self, user, suffix=''):
1248 1248 from rhodecode.model.permission import PermissionModel
1249 1249 return PermissionModel().get_default_perms(user.user_perms, suffix)
1250 1250
1251 1251 def get_default_perms(self, suffix=''):
1252 1252 return self._get_default_perms(self, suffix)
1253 1253
1254 1254 def get_api_data(self, include_secrets=False, details='full'):
1255 1255 """
1256 1256 Common function for generating user related data for API
1257 1257
1258 1258 :param include_secrets: By default secrets in the API data will be replaced
1259 1259 by a placeholder value to prevent exposing this data by accident. In case
1260 1260 this data shall be exposed, set this flag to ``True``.
1261 1261
1262 1262 :param details: details can be 'basic|full' basic gives only a subset of
1263 1263 the available user information that includes user_id, name and emails.
1264 1264 """
1265 1265 user = self
1266 1266 user_data = self.user_data
1267 1267 data = {
1268 1268 'user_id': user.user_id,
1269 1269 'username': user.username,
1270 1270 'firstname': user.name,
1271 1271 'lastname': user.lastname,
1272 1272 'description': user.description,
1273 1273 'email': user.email,
1274 1274 'emails': user.emails,
1275 1275 }
1276 1276 if details == 'basic':
1277 1277 return data
1278 1278
1279 1279 auth_token_length = 40
1280 1280 auth_token_replacement = '*' * auth_token_length
1281 1281
1282 1282 extras = {
1283 1283 'auth_tokens': [auth_token_replacement],
1284 1284 'active': user.active,
1285 1285 'admin': user.admin,
1286 1286 'extern_type': user.extern_type,
1287 1287 'extern_name': user.extern_name,
1288 1288 'last_login': user.last_login,
1289 1289 'last_activity': user.last_activity,
1290 1290 'ip_addresses': user.ip_addresses,
1291 1291 'language': user_data.get('language')
1292 1292 }
1293 1293 data.update(extras)
1294 1294
1295 1295 if include_secrets:
1296 1296 data['auth_tokens'] = user.auth_tokens
1297 1297 return data
1298 1298
1299 1299 def __json__(self):
1300 1300 data = {
1301 1301 'full_name': self.full_name,
1302 1302 'full_name_or_username': self.full_name_or_username,
1303 1303 'short_contact': self.short_contact,
1304 1304 'full_contact': self.full_contact,
1305 1305 }
1306 1306 data.update(self.get_api_data())
1307 1307 return data
1308 1308
1309 1309
1310 1310 class UserApiKeys(Base, BaseModel):
1311 1311 __tablename__ = 'user_api_keys'
1312 1312 __table_args__ = (
1313 1313 Index('uak_api_key_idx', 'api_key'),
1314 1314 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1315 1315 base_table_args
1316 1316 )
1317 1317
1318 1318 # ApiKey role
1319 1319 ROLE_ALL = 'token_role_all'
1320 1320 ROLE_VCS = 'token_role_vcs'
1321 1321 ROLE_API = 'token_role_api'
1322 1322 ROLE_HTTP = 'token_role_http'
1323 1323 ROLE_FEED = 'token_role_feed'
1324 1324 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1325 1325 # The last one is ignored in the list as we only
1326 1326 # use it for one action, and cannot be created by users
1327 1327 ROLE_PASSWORD_RESET = 'token_password_reset'
1328 1328
1329 1329 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1330 1330
1331 1331 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1332 1332 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1333 1333 api_key = Column("api_key", String(255), nullable=False, unique=True)
1334 1334 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1335 1335 expires = Column('expires', Float(53), nullable=False)
1336 1336 role = Column('role', String(255), nullable=True)
1337 1337 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1338 1338
1339 1339 # scope columns
1340 1340 repo_id = Column(
1341 1341 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1342 1342 nullable=True, unique=None, default=None)
1343 1343 repo = relationship('Repository', lazy='joined', back_populates='scoped_tokens')
1344 1344
1345 1345 repo_group_id = Column(
1346 1346 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1347 1347 nullable=True, unique=None, default=None)
1348 1348 repo_group = relationship('RepoGroup', lazy='joined')
1349 1349
1350 1350 user = relationship('User', lazy='joined', back_populates='user_auth_tokens')
1351 1351
1352 1352 def __repr__(self):
1353 1353 return f"<{self.cls_name}('{self.role}')>"
1354 1354
1355 1355 def __json__(self):
1356 1356 data = {
1357 1357 'auth_token': self.api_key,
1358 1358 'role': self.role,
1359 1359 'scope': self.scope_humanized,
1360 1360 'expired': self.expired
1361 1361 }
1362 1362 return data
1363 1363
1364 1364 def get_api_data(self, include_secrets=False):
1365 1365 data = self.__json__()
1366 1366 if include_secrets:
1367 1367 return data
1368 1368 else:
1369 1369 data['auth_token'] = self.token_obfuscated
1370 1370 return data
1371 1371
1372 1372 @hybrid_property
1373 1373 def description_safe(self):
1374 1374 return description_escaper(self.description)
1375 1375
1376 1376 @property
1377 1377 def expired(self):
1378 1378 if self.expires == -1:
1379 1379 return False
1380 1380 return time.time() > self.expires
1381 1381
1382 1382 @classmethod
1383 1383 def _get_role_name(cls, role):
1384 1384 return {
1385 1385 cls.ROLE_ALL: _('all'),
1386 1386 cls.ROLE_HTTP: _('http/web interface'),
1387 1387 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1388 1388 cls.ROLE_API: _('api calls'),
1389 1389 cls.ROLE_FEED: _('feed access'),
1390 1390 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1391 1391 }.get(role, role)
1392 1392
1393 1393 @classmethod
1394 1394 def _get_role_description(cls, role):
1395 1395 return {
1396 1396 cls.ROLE_ALL: _('Token for all actions.'),
1397 1397 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1398 1398 'login using `api_access_controllers_whitelist` functionality.'),
1399 1399 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1400 1400 'Requires auth_token authentication plugin to be active. <br/>'
1401 1401 'Such Token should be used then instead of a password to '
1402 1402 'interact with a repository, and additionally can be '
1403 1403 'limited to single repository using repo scope.'),
1404 1404 cls.ROLE_API: _('Token limited to api calls.'),
1405 1405 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1406 1406 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1407 1407 }.get(role, role)
1408 1408
1409 1409 @property
1410 1410 def role_humanized(self):
1411 1411 return self._get_role_name(self.role)
1412 1412
1413 1413 def _get_scope(self):
1414 1414 if self.repo:
1415 1415 return 'Repository: {}'.format(self.repo.repo_name)
1416 1416 if self.repo_group:
1417 1417 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1418 1418 return 'Global'
1419 1419
1420 1420 @property
1421 1421 def scope_humanized(self):
1422 1422 return self._get_scope()
1423 1423
1424 1424 @property
1425 1425 def token_obfuscated(self):
1426 1426 if self.api_key:
1427 1427 return self.api_key[:4] + "****"
1428 1428
1429 1429
1430 1430 class UserEmailMap(Base, BaseModel):
1431 1431 __tablename__ = 'user_email_map'
1432 1432 __table_args__ = (
1433 1433 Index('uem_email_idx', 'email'),
1434 1434 Index('uem_user_id_idx', 'user_id'),
1435 1435 UniqueConstraint('email'),
1436 1436 base_table_args
1437 1437 )
1438 1438
1439 1439 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1440 1440 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1441 1441 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1442 1442 user = relationship('User', lazy='joined', back_populates='user_emails')
1443 1443
1444 1444 @validates('_email')
1445 1445 def validate_email(self, key, email):
1446 1446 # check if this email is not main one
1447 1447 main_email = Session().query(User).filter(User.email == email).scalar()
1448 1448 if main_email is not None:
1449 1449 raise AttributeError('email %s is present is user table' % email)
1450 1450 return email
1451 1451
1452 1452 @hybrid_property
1453 1453 def email(self):
1454 1454 return self._email
1455 1455
1456 1456 @email.setter
1457 1457 def email(self, val):
1458 1458 self._email = val.lower() if val else None
1459 1459
1460 1460
1461 1461 class UserIpMap(Base, BaseModel):
1462 1462 __tablename__ = 'user_ip_map'
1463 1463 __table_args__ = (
1464 1464 UniqueConstraint('user_id', 'ip_addr'),
1465 1465 base_table_args
1466 1466 )
1467 1467
1468 1468 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1469 1469 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1470 1470 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1471 1471 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1472 1472 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1473 1473 user = relationship('User', lazy='joined', back_populates='user_ip_map')
1474 1474
1475 1475 @hybrid_property
1476 1476 def description_safe(self):
1477 1477 return description_escaper(self.description)
1478 1478
1479 1479 @classmethod
1480 1480 def _get_ip_range(cls, ip_addr):
1481 1481 net = ipaddress.ip_network(safe_str(ip_addr), strict=False)
1482 1482 return [str(net.network_address), str(net.broadcast_address)]
1483 1483
1484 1484 def __json__(self):
1485 1485 return {
1486 1486 'ip_addr': self.ip_addr,
1487 1487 'ip_range': self._get_ip_range(self.ip_addr),
1488 1488 }
1489 1489
1490 1490 def __repr__(self):
1491 1491 return f"<{self.cls_name}('user_id={self.user_id} => ip={self.ip_addr}')>"
1492 1492
1493 1493
1494 1494 class UserSshKeys(Base, BaseModel):
1495 1495 __tablename__ = 'user_ssh_keys'
1496 1496 __table_args__ = (
1497 1497 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1498 1498
1499 1499 UniqueConstraint('ssh_key_fingerprint'),
1500 1500
1501 1501 base_table_args
1502 1502 )
1503 1503
1504 1504 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1505 1505 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1506 1506 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1507 1507
1508 1508 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1509 1509
1510 1510 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1511 1511 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1512 1512 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1513 1513
1514 1514 user = relationship('User', lazy='joined', back_populates='user_ssh_keys')
1515 1515
1516 1516 def __json__(self):
1517 1517 data = {
1518 1518 'ssh_fingerprint': self.ssh_key_fingerprint,
1519 1519 'description': self.description,
1520 1520 'created_on': self.created_on
1521 1521 }
1522 1522 return data
1523 1523
1524 1524 def get_api_data(self):
1525 1525 data = self.__json__()
1526 1526 return data
1527 1527
1528 1528
1529 1529 class UserLog(Base, BaseModel):
1530 1530 __tablename__ = 'user_logs'
1531 1531 __table_args__ = (
1532 1532 base_table_args,
1533 1533 )
1534 1534
1535 1535 VERSION_1 = 'v1'
1536 1536 VERSION_2 = 'v2'
1537 1537 VERSIONS = [VERSION_1, VERSION_2]
1538 1538
1539 1539 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1540 1540 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1541 1541 username = Column("username", String(255), nullable=True, unique=None, default=None)
1542 1542 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1543 1543 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1544 1544 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1545 1545 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1546 1546 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1547 1547
1548 1548 version = Column("version", String(255), nullable=True, default=VERSION_1)
1549 1549 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1550 1550 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1551 1551 user = relationship('User', cascade='', back_populates='user_log')
1552 1552 repository = relationship('Repository', cascade='', back_populates='logs')
1553 1553
1554 1554 def __repr__(self):
1555 1555 return f"<{self.cls_name}('id:{self.repository_name}:{self.action}')>"
1556 1556
1557 1557 def __json__(self):
1558 1558 return {
1559 1559 'user_id': self.user_id,
1560 1560 'username': self.username,
1561 1561 'repository_id': self.repository_id,
1562 1562 'repository_name': self.repository_name,
1563 1563 'user_ip': self.user_ip,
1564 1564 'action_date': self.action_date,
1565 1565 'action': self.action,
1566 1566 }
1567 1567
1568 1568 @hybrid_property
1569 1569 def entry_id(self):
1570 1570 return self.user_log_id
1571 1571
1572 1572 @property
1573 1573 def action_as_day(self):
1574 1574 return datetime.date(*self.action_date.timetuple()[:3])
1575 1575
1576 1576
1577 1577 class UserGroup(Base, BaseModel):
1578 1578 __tablename__ = 'users_groups'
1579 1579 __table_args__ = (
1580 1580 base_table_args,
1581 1581 )
1582 1582
1583 1583 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1584 1584 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1585 1585 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1586 1586 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1587 1587 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1588 1588 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1589 1589 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1590 1590 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1591 1591
1592 1592 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined", back_populates='users_group')
1593 1593 users_group_to_perm = relationship('UserGroupToPerm', cascade='all', back_populates='users_group')
1594 1594 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='users_group')
1595 1595 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='users_group')
1596 1596 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all', back_populates='user_group')
1597 1597
1598 1598 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all', back_populates='target_user_group')
1599 1599
1600 1600 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all', back_populates='users_group')
1601 1601 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id", back_populates='user_groups')
1602 1602
1603 1603 @classmethod
1604 1604 def _load_group_data(cls, column):
1605 1605 if not column:
1606 1606 return {}
1607 1607
1608 1608 try:
1609 1609 return json.loads(column) or {}
1610 1610 except TypeError:
1611 1611 return {}
1612 1612
1613 1613 @hybrid_property
1614 1614 def description_safe(self):
1615 1615 return description_escaper(self.user_group_description)
1616 1616
1617 1617 @hybrid_property
1618 1618 def group_data(self):
1619 1619 return self._load_group_data(self._group_data)
1620 1620
1621 1621 @group_data.expression
1622 1622 def group_data(self, **kwargs):
1623 1623 return self._group_data
1624 1624
1625 1625 @group_data.setter
1626 1626 def group_data(self, val):
1627 1627 try:
1628 1628 self._group_data = json.dumps(val)
1629 1629 except Exception:
1630 1630 log.error(traceback.format_exc())
1631 1631
1632 1632 @classmethod
1633 1633 def _load_sync(cls, group_data):
1634 1634 if group_data:
1635 1635 return group_data.get('extern_type')
1636 1636
1637 1637 @property
1638 1638 def sync(self):
1639 1639 return self._load_sync(self.group_data)
1640 1640
1641 1641 def __repr__(self):
1642 1642 return f"<{self.cls_name}('id:{self.users_group_id}:{self.users_group_name}')>"
1643 1643
1644 1644 @classmethod
1645 1645 def get_by_group_name(cls, group_name, cache=False,
1646 1646 case_insensitive=False):
1647 1647 if case_insensitive:
1648 1648 q = cls.query().filter(func.lower(cls.users_group_name) ==
1649 1649 func.lower(group_name))
1650 1650
1651 1651 else:
1652 1652 q = cls.query().filter(cls.users_group_name == group_name)
1653 1653 if cache:
1654 1654 name_key = _hash_key(group_name)
1655 1655 q = q.options(
1656 1656 FromCache("sql_cache_short", f"get_group_{name_key}"))
1657 1657 return q.scalar()
1658 1658
1659 1659 @classmethod
1660 1660 def get(cls, user_group_id, cache=False):
1661 1661 if not user_group_id:
1662 1662 return
1663 1663
1664 1664 user_group = cls.query()
1665 1665 if cache:
1666 1666 user_group = user_group.options(
1667 1667 FromCache("sql_cache_short", f"get_users_group_{user_group_id}"))
1668 1668 return user_group.get(user_group_id)
1669 1669
1670 1670 def permissions(self, with_admins=True, with_owner=True,
1671 1671 expand_from_user_groups=False):
1672 1672 """
1673 1673 Permissions for user groups
1674 1674 """
1675 1675 _admin_perm = 'usergroup.admin'
1676 1676
1677 1677 owner_row = []
1678 1678 if with_owner:
1679 1679 usr = AttributeDict(self.user.get_dict())
1680 1680 usr.owner_row = True
1681 1681 usr.permission = _admin_perm
1682 1682 owner_row.append(usr)
1683 1683
1684 1684 super_admin_ids = []
1685 1685 super_admin_rows = []
1686 1686 if with_admins:
1687 1687 for usr in User.get_all_super_admins():
1688 1688 super_admin_ids.append(usr.user_id)
1689 1689 # if this admin is also owner, don't double the record
1690 1690 if usr.user_id == owner_row[0].user_id:
1691 1691 owner_row[0].admin_row = True
1692 1692 else:
1693 1693 usr = AttributeDict(usr.get_dict())
1694 1694 usr.admin_row = True
1695 1695 usr.permission = _admin_perm
1696 1696 super_admin_rows.append(usr)
1697 1697
1698 1698 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1699 1699 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1700 1700 joinedload(UserUserGroupToPerm.user),
1701 1701 joinedload(UserUserGroupToPerm.permission),)
1702 1702
1703 1703 # get owners and admins and permissions. We do a trick of re-writing
1704 1704 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1705 1705 # has a global reference and changing one object propagates to all
1706 1706 # others. This means if admin is also an owner admin_row that change
1707 1707 # would propagate to both objects
1708 1708 perm_rows = []
1709 1709 for _usr in q.all():
1710 1710 usr = AttributeDict(_usr.user.get_dict())
1711 1711 # if this user is also owner/admin, mark as duplicate record
1712 1712 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1713 1713 usr.duplicate_perm = True
1714 1714 usr.permission = _usr.permission.permission_name
1715 1715 perm_rows.append(usr)
1716 1716
1717 1717 # filter the perm rows by 'default' first and then sort them by
1718 1718 # admin,write,read,none permissions sorted again alphabetically in
1719 1719 # each group
1720 1720 perm_rows = sorted(perm_rows, key=display_user_sort)
1721 1721
1722 1722 user_groups_rows = []
1723 1723 if expand_from_user_groups:
1724 1724 for ug in self.permission_user_groups(with_members=True):
1725 1725 for user_data in ug.members:
1726 1726 user_groups_rows.append(user_data)
1727 1727
1728 1728 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1729 1729
1730 1730 def permission_user_groups(self, with_members=False):
1731 1731 q = UserGroupUserGroupToPerm.query()\
1732 1732 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1733 1733 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1734 1734 joinedload(UserGroupUserGroupToPerm.target_user_group),
1735 1735 joinedload(UserGroupUserGroupToPerm.permission),)
1736 1736
1737 1737 perm_rows = []
1738 1738 for _user_group in q.all():
1739 1739 entry = AttributeDict(_user_group.user_group.get_dict())
1740 1740 entry.permission = _user_group.permission.permission_name
1741 1741 if with_members:
1742 1742 entry.members = [x.user.get_dict()
1743 1743 for x in _user_group.user_group.members]
1744 1744 perm_rows.append(entry)
1745 1745
1746 1746 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1747 1747 return perm_rows
1748 1748
1749 1749 def _get_default_perms(self, user_group, suffix=''):
1750 1750 from rhodecode.model.permission import PermissionModel
1751 1751 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1752 1752
1753 1753 def get_default_perms(self, suffix=''):
1754 1754 return self._get_default_perms(self, suffix)
1755 1755
1756 1756 def get_api_data(self, with_group_members=True, include_secrets=False):
1757 1757 """
1758 1758 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1759 1759 basically forwarded.
1760 1760
1761 1761 """
1762 1762 user_group = self
1763 1763 data = {
1764 1764 'users_group_id': user_group.users_group_id,
1765 1765 'group_name': user_group.users_group_name,
1766 1766 'group_description': user_group.user_group_description,
1767 1767 'active': user_group.users_group_active,
1768 1768 'owner': user_group.user.username,
1769 1769 'sync': user_group.sync,
1770 1770 'owner_email': user_group.user.email,
1771 1771 }
1772 1772
1773 1773 if with_group_members:
1774 1774 users = []
1775 1775 for user in user_group.members:
1776 1776 user = user.user
1777 1777 users.append(user.get_api_data(include_secrets=include_secrets))
1778 1778 data['users'] = users
1779 1779
1780 1780 return data
1781 1781
1782 1782
1783 1783 class UserGroupMember(Base, BaseModel):
1784 1784 __tablename__ = 'users_groups_members'
1785 1785 __table_args__ = (
1786 1786 base_table_args,
1787 1787 )
1788 1788
1789 1789 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1790 1790 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1791 1791 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1792 1792
1793 1793 user = relationship('User', lazy='joined', back_populates='group_member')
1794 1794 users_group = relationship('UserGroup', back_populates='members')
1795 1795
1796 1796 def __init__(self, gr_id='', u_id=''):
1797 1797 self.users_group_id = gr_id
1798 1798 self.user_id = u_id
1799 1799
1800 1800
1801 1801 class RepositoryField(Base, BaseModel):
1802 1802 __tablename__ = 'repositories_fields'
1803 1803 __table_args__ = (
1804 1804 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1805 1805 base_table_args,
1806 1806 )
1807 1807
1808 1808 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1809 1809
1810 1810 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1811 1811 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1812 1812 field_key = Column("field_key", String(250))
1813 1813 field_label = Column("field_label", String(1024), nullable=False)
1814 1814 field_value = Column("field_value", String(10000), nullable=False)
1815 1815 field_desc = Column("field_desc", String(1024), nullable=False)
1816 1816 field_type = Column("field_type", String(255), nullable=False, unique=None)
1817 1817 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1818 1818
1819 1819 repository = relationship('Repository', back_populates='extra_fields')
1820 1820
1821 1821 @property
1822 1822 def field_key_prefixed(self):
1823 1823 return 'ex_%s' % self.field_key
1824 1824
1825 1825 @classmethod
1826 1826 def un_prefix_key(cls, key):
1827 1827 if key.startswith(cls.PREFIX):
1828 1828 return key[len(cls.PREFIX):]
1829 1829 return key
1830 1830
1831 1831 @classmethod
1832 1832 def get_by_key_name(cls, key, repo):
1833 1833 row = cls.query()\
1834 1834 .filter(cls.repository == repo)\
1835 1835 .filter(cls.field_key == key).scalar()
1836 1836 return row
1837 1837
1838 1838
1839 1839 class Repository(Base, BaseModel):
1840 1840 __tablename__ = 'repositories'
1841 1841 __table_args__ = (
1842 1842 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1843 1843 base_table_args,
1844 1844 )
1845 1845 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1846 1846 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1847 1847 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1848 1848
1849 1849 STATE_CREATED = 'repo_state_created'
1850 1850 STATE_PENDING = 'repo_state_pending'
1851 1851 STATE_ERROR = 'repo_state_error'
1852 1852
1853 1853 LOCK_AUTOMATIC = 'lock_auto'
1854 1854 LOCK_API = 'lock_api'
1855 1855 LOCK_WEB = 'lock_web'
1856 1856 LOCK_PULL = 'lock_pull'
1857 1857
1858 1858 NAME_SEP = URL_SEP
1859 1859
1860 1860 repo_id = Column(
1861 1861 "repo_id", Integer(), nullable=False, unique=True, default=None,
1862 1862 primary_key=True)
1863 1863 _repo_name = Column(
1864 1864 "repo_name", Text(), nullable=False, default=None)
1865 1865 repo_name_hash = Column(
1866 1866 "repo_name_hash", String(255), nullable=False, unique=True)
1867 1867 repo_state = Column("repo_state", String(255), nullable=True)
1868 1868
1869 1869 clone_uri = Column(
1870 1870 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1871 1871 default=None)
1872 1872 push_uri = Column(
1873 1873 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1874 1874 default=None)
1875 1875 repo_type = Column(
1876 1876 "repo_type", String(255), nullable=False, unique=False, default=None)
1877 1877 user_id = Column(
1878 1878 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1879 1879 unique=False, default=None)
1880 1880 private = Column(
1881 1881 "private", Boolean(), nullable=True, unique=None, default=None)
1882 1882 archived = Column(
1883 1883 "archived", Boolean(), nullable=True, unique=None, default=None)
1884 1884 enable_statistics = Column(
1885 1885 "statistics", Boolean(), nullable=True, unique=None, default=True)
1886 1886 enable_downloads = Column(
1887 1887 "downloads", Boolean(), nullable=True, unique=None, default=True)
1888 1888 description = Column(
1889 1889 "description", String(10000), nullable=True, unique=None, default=None)
1890 1890 created_on = Column(
1891 1891 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1892 1892 default=datetime.datetime.now)
1893 1893 updated_on = Column(
1894 1894 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1895 1895 default=datetime.datetime.now)
1896 1896 _landing_revision = Column(
1897 1897 "landing_revision", String(255), nullable=False, unique=False,
1898 1898 default=None)
1899 1899 enable_locking = Column(
1900 1900 "enable_locking", Boolean(), nullable=False, unique=None,
1901 1901 default=False)
1902 1902 _locked = Column(
1903 1903 "locked", String(255), nullable=True, unique=False, default=None)
1904 1904 _changeset_cache = Column(
1905 1905 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1906 1906
1907 1907 fork_id = Column(
1908 1908 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1909 1909 nullable=True, unique=False, default=None)
1910 1910 group_id = Column(
1911 1911 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1912 1912 unique=False, default=None)
1913 1913
1914 1914 user = relationship('User', lazy='joined', back_populates='repositories')
1915 1915 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1916 1916 group = relationship('RepoGroup', lazy='joined')
1917 1917 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
1918 1918 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='repository')
1919 1919 stats = relationship('Statistics', cascade='all', uselist=False)
1920 1920
1921 1921 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all', back_populates='follows_repository')
1922 1922 extra_fields = relationship('RepositoryField', cascade="all, delete-orphan", back_populates='repository')
1923 1923
1924 1924 logs = relationship('UserLog', back_populates='repository')
1925 1925
1926 1926 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='repo')
1927 1927
1928 1928 pull_requests_source = relationship(
1929 1929 'PullRequest',
1930 1930 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1931 1931 cascade="all, delete-orphan",
1932 1932 overlaps="source_repo"
1933 1933 )
1934 1934 pull_requests_target = relationship(
1935 1935 'PullRequest',
1936 1936 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1937 1937 cascade="all, delete-orphan",
1938 1938 overlaps="target_repo"
1939 1939 )
1940 1940
1941 1941 ui = relationship('RepoRhodeCodeUi', cascade="all")
1942 1942 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1943 1943 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo')
1944 1944
1945 1945 scoped_tokens = relationship('UserApiKeys', cascade="all", back_populates='repo')
1946 1946
1947 1947 # no cascade, set NULL
1948 1948 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id', viewonly=True)
1949 1949
1950 1950 review_rules = relationship('RepoReviewRule')
1951 1951 user_branch_perms = relationship('UserToRepoBranchPermission')
1952 1952 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission')
1953 1953
1954 1954 def __repr__(self):
1955 1955 return "<%s('%s:%s')>" % (self.cls_name, self.repo_id, self.repo_name)
1956 1956
1957 1957 @hybrid_property
1958 1958 def description_safe(self):
1959 1959 return description_escaper(self.description)
1960 1960
1961 1961 @hybrid_property
1962 1962 def landing_rev(self):
1963 1963 # always should return [rev_type, rev], e.g ['branch', 'master']
1964 1964 if self._landing_revision:
1965 1965 _rev_info = self._landing_revision.split(':')
1966 1966 if len(_rev_info) < 2:
1967 1967 _rev_info.insert(0, 'rev')
1968 1968 return [_rev_info[0], _rev_info[1]]
1969 1969 return [None, None]
1970 1970
1971 1971 @property
1972 1972 def landing_ref_type(self):
1973 1973 return self.landing_rev[0]
1974 1974
1975 1975 @property
1976 1976 def landing_ref_name(self):
1977 1977 return self.landing_rev[1]
1978 1978
1979 1979 @landing_rev.setter
1980 1980 def landing_rev(self, val):
1981 1981 if ':' not in val:
1982 1982 raise ValueError('value must be delimited with `:` and consist '
1983 1983 'of <rev_type>:<rev>, got %s instead' % val)
1984 1984 self._landing_revision = val
1985 1985
1986 1986 @hybrid_property
1987 1987 def locked(self):
1988 1988 if self._locked:
1989 1989 user_id, timelocked, reason = self._locked.split(':')
1990 1990 lock_values = int(user_id), timelocked, reason
1991 1991 else:
1992 1992 lock_values = [None, None, None]
1993 1993 return lock_values
1994 1994
1995 1995 @locked.setter
1996 1996 def locked(self, val):
1997 1997 if val and isinstance(val, (list, tuple)):
1998 1998 self._locked = ':'.join(map(str, val))
1999 1999 else:
2000 2000 self._locked = None
2001 2001
2002 2002 @classmethod
2003 2003 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2004 2004 from rhodecode.lib.vcs.backends.base import EmptyCommit
2005 2005 dummy = EmptyCommit().__json__()
2006 2006 if not changeset_cache_raw:
2007 2007 dummy['source_repo_id'] = repo_id
2008 2008 return json.loads(json.dumps(dummy))
2009 2009
2010 2010 try:
2011 2011 return json.loads(changeset_cache_raw)
2012 2012 except TypeError:
2013 2013 return dummy
2014 2014 except Exception:
2015 2015 log.error(traceback.format_exc())
2016 2016 return dummy
2017 2017
2018 2018 @hybrid_property
2019 2019 def changeset_cache(self):
2020 2020 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
2021 2021
2022 2022 @changeset_cache.setter
2023 2023 def changeset_cache(self, val):
2024 2024 try:
2025 2025 self._changeset_cache = json.dumps(val)
2026 2026 except Exception:
2027 2027 log.error(traceback.format_exc())
2028 2028
2029 2029 @hybrid_property
2030 2030 def repo_name(self):
2031 2031 return self._repo_name
2032 2032
2033 2033 @repo_name.setter
2034 2034 def repo_name(self, value):
2035 2035 self._repo_name = value
2036 2036 self.repo_name_hash = sha1(safe_bytes(value))
2037 2037
2038 2038 @classmethod
2039 2039 def normalize_repo_name(cls, repo_name):
2040 2040 """
2041 2041 Normalizes os specific repo_name to the format internally stored inside
2042 2042 database using URL_SEP
2043 2043
2044 2044 :param cls:
2045 2045 :param repo_name:
2046 2046 """
2047 2047 return cls.NAME_SEP.join(repo_name.split(os.sep))
2048 2048
2049 2049 @classmethod
2050 2050 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
2051 2051 session = Session()
2052 2052 q = session.query(cls).filter(cls.repo_name == repo_name)
2053 2053
2054 2054 if cache:
2055 2055 if identity_cache:
2056 2056 val = cls.identity_cache(session, 'repo_name', repo_name)
2057 2057 if val:
2058 2058 return val
2059 2059 else:
2060 2060 cache_key = f"get_repo_by_name_{_hash_key(repo_name)}"
2061 2061 q = q.options(
2062 2062 FromCache("sql_cache_short", cache_key))
2063 2063
2064 2064 return q.scalar()
2065 2065
2066 2066 @classmethod
2067 2067 def get_by_id_or_repo_name(cls, repoid):
2068 2068 if isinstance(repoid, int):
2069 2069 try:
2070 2070 repo = cls.get(repoid)
2071 2071 except ValueError:
2072 2072 repo = None
2073 2073 else:
2074 2074 repo = cls.get_by_repo_name(repoid)
2075 2075 return repo
2076 2076
2077 2077 @classmethod
2078 2078 def get_by_full_path(cls, repo_full_path):
2079 2079 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
2080 2080 repo_name = cls.normalize_repo_name(repo_name)
2081 2081 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
2082 2082
2083 2083 @classmethod
2084 2084 def get_repo_forks(cls, repo_id):
2085 2085 return cls.query().filter(Repository.fork_id == repo_id)
2086 2086
2087 2087 @classmethod
2088 2088 def base_path(cls):
2089 2089 """
2090 2090 Returns base path when all repos are stored
2091 2091
2092 2092 :param cls:
2093 2093 """
2094 2094 from rhodecode.lib.utils import get_rhodecode_repo_store_path
2095 2095 return get_rhodecode_repo_store_path()
2096 2096
2097 2097 @classmethod
2098 2098 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
2099 2099 case_insensitive=True, archived=False):
2100 2100 q = Repository.query()
2101 2101
2102 2102 if not archived:
2103 2103 q = q.filter(Repository.archived.isnot(true()))
2104 2104
2105 2105 if not isinstance(user_id, Optional):
2106 2106 q = q.filter(Repository.user_id == user_id)
2107 2107
2108 2108 if not isinstance(group_id, Optional):
2109 2109 q = q.filter(Repository.group_id == group_id)
2110 2110
2111 2111 if case_insensitive:
2112 2112 q = q.order_by(func.lower(Repository.repo_name))
2113 2113 else:
2114 2114 q = q.order_by(Repository.repo_name)
2115 2115
2116 2116 return q.all()
2117 2117
2118 2118 @property
2119 2119 def repo_uid(self):
2120 2120 return '_{}'.format(self.repo_id)
2121 2121
2122 2122 @property
2123 2123 def forks(self):
2124 2124 """
2125 2125 Return forks of this repo
2126 2126 """
2127 2127 return Repository.get_repo_forks(self.repo_id)
2128 2128
2129 2129 @property
2130 2130 def parent(self):
2131 2131 """
2132 2132 Returns fork parent
2133 2133 """
2134 2134 return self.fork
2135 2135
2136 2136 @property
2137 2137 def just_name(self):
2138 2138 return self.repo_name.split(self.NAME_SEP)[-1]
2139 2139
2140 2140 @property
2141 2141 def groups_with_parents(self):
2142 2142 groups = []
2143 2143 if self.group is None:
2144 2144 return groups
2145 2145
2146 2146 cur_gr = self.group
2147 2147 groups.insert(0, cur_gr)
2148 2148 while 1:
2149 2149 gr = getattr(cur_gr, 'parent_group', None)
2150 2150 cur_gr = cur_gr.parent_group
2151 2151 if gr is None:
2152 2152 break
2153 2153 groups.insert(0, gr)
2154 2154
2155 2155 return groups
2156 2156
2157 2157 @property
2158 2158 def groups_and_repo(self):
2159 2159 return self.groups_with_parents, self
2160 2160
2161 2161 @property
2162 2162 def repo_path(self):
2163 2163 """
2164 2164 Returns base full path for that repository means where it actually
2165 2165 exists on a filesystem
2166 2166 """
2167 2167 return self.base_path()
2168 2168
2169 2169 @property
2170 2170 def repo_full_path(self):
2171 2171 p = [self.repo_path]
2172 2172 # we need to split the name by / since this is how we store the
2173 2173 # names in the database, but that eventually needs to be converted
2174 2174 # into a valid system path
2175 2175 p += self.repo_name.split(self.NAME_SEP)
2176 2176 return os.path.join(*map(safe_str, p))
2177 2177
2178 2178 @property
2179 2179 def cache_keys(self):
2180 2180 """
2181 2181 Returns associated cache keys for that repo
2182 2182 """
2183 2183 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2184 2184 return CacheKey.query()\
2185 2185 .filter(CacheKey.cache_key == repo_namespace_key)\
2186 2186 .order_by(CacheKey.cache_key)\
2187 2187 .all()
2188 2188
2189 2189 @property
2190 2190 def cached_diffs_relative_dir(self):
2191 2191 """
2192 2192 Return a relative to the repository store path of cached diffs
2193 2193 used for safe display for users, who shouldn't know the absolute store
2194 2194 path
2195 2195 """
2196 2196 return os.path.join(
2197 2197 os.path.dirname(self.repo_name),
2198 2198 self.cached_diffs_dir.split(os.path.sep)[-1])
2199 2199
2200 2200 @property
2201 2201 def cached_diffs_dir(self):
2202 2202 path = self.repo_full_path
2203 2203 return os.path.join(
2204 2204 os.path.dirname(path),
2205 2205 f'.__shadow_diff_cache_repo_{self.repo_id}')
2206 2206
2207 2207 def cached_diffs(self):
2208 2208 diff_cache_dir = self.cached_diffs_dir
2209 2209 if os.path.isdir(diff_cache_dir):
2210 2210 return os.listdir(diff_cache_dir)
2211 2211 return []
2212 2212
2213 2213 def shadow_repos(self):
2214 2214 shadow_repos_pattern = f'.__shadow_repo_{self.repo_id}'
2215 2215 return [
2216 2216 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2217 2217 if x.startswith(shadow_repos_pattern)
2218 2218 ]
2219 2219
2220 2220 def get_new_name(self, repo_name):
2221 2221 """
2222 2222 returns new full repository name based on assigned group and new new
2223 2223
2224 2224 :param repo_name:
2225 2225 """
2226 2226 path_prefix = self.group.full_path_splitted if self.group else []
2227 2227 return self.NAME_SEP.join(path_prefix + [repo_name])
2228 2228
2229 2229 @property
2230 2230 def _config(self):
2231 2231 """
2232 2232 Returns db based config object.
2233 2233 """
2234 2234 from rhodecode.lib.utils import make_db_config
2235 2235 return make_db_config(clear_session=False, repo=self)
2236 2236
2237 2237 def permissions(self, with_admins=True, with_owner=True,
2238 2238 expand_from_user_groups=False):
2239 2239 """
2240 2240 Permissions for repositories
2241 2241 """
2242 2242 _admin_perm = 'repository.admin'
2243 2243
2244 2244 owner_row = []
2245 2245 if with_owner:
2246 2246 usr = AttributeDict(self.user.get_dict())
2247 2247 usr.owner_row = True
2248 2248 usr.permission = _admin_perm
2249 2249 usr.permission_id = None
2250 2250 owner_row.append(usr)
2251 2251
2252 2252 super_admin_ids = []
2253 2253 super_admin_rows = []
2254 2254 if with_admins:
2255 2255 for usr in User.get_all_super_admins():
2256 2256 super_admin_ids.append(usr.user_id)
2257 2257 # if this admin is also owner, don't double the record
2258 2258 if usr.user_id == owner_row[0].user_id:
2259 2259 owner_row[0].admin_row = True
2260 2260 else:
2261 2261 usr = AttributeDict(usr.get_dict())
2262 2262 usr.admin_row = True
2263 2263 usr.permission = _admin_perm
2264 2264 usr.permission_id = None
2265 2265 super_admin_rows.append(usr)
2266 2266
2267 2267 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2268 2268 q = q.options(joinedload(UserRepoToPerm.repository),
2269 2269 joinedload(UserRepoToPerm.user),
2270 2270 joinedload(UserRepoToPerm.permission),)
2271 2271
2272 2272 # get owners and admins and permissions. We do a trick of re-writing
2273 2273 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2274 2274 # has a global reference and changing one object propagates to all
2275 2275 # others. This means if admin is also an owner admin_row that change
2276 2276 # would propagate to both objects
2277 2277 perm_rows = []
2278 2278 for _usr in q.all():
2279 2279 usr = AttributeDict(_usr.user.get_dict())
2280 2280 # if this user is also owner/admin, mark as duplicate record
2281 2281 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2282 2282 usr.duplicate_perm = True
2283 2283 # also check if this permission is maybe used by branch_permissions
2284 2284 if _usr.branch_perm_entry:
2285 2285 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2286 2286
2287 2287 usr.permission = _usr.permission.permission_name
2288 2288 usr.permission_id = _usr.repo_to_perm_id
2289 2289 perm_rows.append(usr)
2290 2290
2291 2291 # filter the perm rows by 'default' first and then sort them by
2292 2292 # admin,write,read,none permissions sorted again alphabetically in
2293 2293 # each group
2294 2294 perm_rows = sorted(perm_rows, key=display_user_sort)
2295 2295
2296 2296 user_groups_rows = []
2297 2297 if expand_from_user_groups:
2298 2298 for ug in self.permission_user_groups(with_members=True):
2299 2299 for user_data in ug.members:
2300 2300 user_groups_rows.append(user_data)
2301 2301
2302 2302 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2303 2303
2304 2304 def permission_user_groups(self, with_members=True):
2305 2305 q = UserGroupRepoToPerm.query()\
2306 2306 .filter(UserGroupRepoToPerm.repository == self)
2307 2307 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2308 2308 joinedload(UserGroupRepoToPerm.users_group),
2309 2309 joinedload(UserGroupRepoToPerm.permission),)
2310 2310
2311 2311 perm_rows = []
2312 2312 for _user_group in q.all():
2313 2313 entry = AttributeDict(_user_group.users_group.get_dict())
2314 2314 entry.permission = _user_group.permission.permission_name
2315 2315 if with_members:
2316 2316 entry.members = [x.user.get_dict()
2317 2317 for x in _user_group.users_group.members]
2318 2318 perm_rows.append(entry)
2319 2319
2320 2320 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2321 2321 return perm_rows
2322 2322
2323 2323 def get_api_data(self, include_secrets=False):
2324 2324 """
2325 2325 Common function for generating repo api data
2326 2326
2327 2327 :param include_secrets: See :meth:`User.get_api_data`.
2328 2328
2329 2329 """
2330 2330 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2331 2331 # move this methods on models level.
2332 2332 from rhodecode.model.settings import SettingsModel
2333 2333 from rhodecode.model.repo import RepoModel
2334 2334
2335 2335 repo = self
2336 2336 _user_id, _time, _reason = self.locked
2337 2337
2338 2338 data = {
2339 2339 'repo_id': repo.repo_id,
2340 2340 'repo_name': repo.repo_name,
2341 2341 'repo_type': repo.repo_type,
2342 2342 'clone_uri': repo.clone_uri or '',
2343 2343 'push_uri': repo.push_uri or '',
2344 2344 'url': RepoModel().get_url(self),
2345 2345 'private': repo.private,
2346 2346 'created_on': repo.created_on,
2347 2347 'description': repo.description_safe,
2348 2348 'landing_rev': repo.landing_rev,
2349 2349 'owner': repo.user.username,
2350 2350 'fork_of': repo.fork.repo_name if repo.fork else None,
2351 2351 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2352 2352 'enable_statistics': repo.enable_statistics,
2353 2353 'enable_locking': repo.enable_locking,
2354 2354 'enable_downloads': repo.enable_downloads,
2355 2355 'last_changeset': repo.changeset_cache,
2356 2356 'locked_by': User.get(_user_id).get_api_data(
2357 2357 include_secrets=include_secrets) if _user_id else None,
2358 2358 'locked_date': time_to_datetime(_time) if _time else None,
2359 2359 'lock_reason': _reason if _reason else None,
2360 2360 }
2361 2361
2362 2362 # TODO: mikhail: should be per-repo settings here
2363 2363 rc_config = SettingsModel().get_all_settings()
2364 2364 repository_fields = str2bool(
2365 2365 rc_config.get('rhodecode_repository_fields'))
2366 2366 if repository_fields:
2367 2367 for f in self.extra_fields:
2368 2368 data[f.field_key_prefixed] = f.field_value
2369 2369
2370 2370 return data
2371 2371
2372 2372 @classmethod
2373 2373 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2374 2374 if not lock_time:
2375 2375 lock_time = time.time()
2376 2376 if not lock_reason:
2377 2377 lock_reason = cls.LOCK_AUTOMATIC
2378 2378 repo.locked = [user_id, lock_time, lock_reason]
2379 2379 Session().add(repo)
2380 2380 Session().commit()
2381 2381
2382 2382 @classmethod
2383 2383 def unlock(cls, repo):
2384 2384 repo.locked = None
2385 2385 Session().add(repo)
2386 2386 Session().commit()
2387 2387
2388 2388 @classmethod
2389 2389 def getlock(cls, repo):
2390 2390 return repo.locked
2391 2391
2392 2392 def get_locking_state(self, action, user_id, only_when_enabled=True):
2393 2393 """
2394 2394 Checks locking on this repository, if locking is enabled and lock is
2395 2395 present returns a tuple of make_lock, locked, locked_by.
2396 2396 make_lock can have 3 states None (do nothing) True, make lock
2397 2397 False release lock, This value is later propagated to hooks, which
2398 2398 do the locking. Think about this as signals passed to hooks what to do.
2399 2399
2400 2400 """
2401 2401 # TODO: johbo: This is part of the business logic and should be moved
2402 2402 # into the RepositoryModel.
2403 2403
2404 2404 if action not in ('push', 'pull'):
2405 2405 raise ValueError("Invalid action value: %s" % repr(action))
2406 2406
2407 2407 # defines if locked error should be thrown to user
2408 2408 currently_locked = False
2409 2409 # defines if new lock should be made, tri-state
2410 2410 make_lock = None
2411 2411 repo = self
2412 2412 user = User.get(user_id)
2413 2413
2414 2414 lock_info = repo.locked
2415 2415
2416 2416 if repo and (repo.enable_locking or not only_when_enabled):
2417 2417 if action == 'push':
2418 2418 # check if it's already locked !, if it is compare users
2419 2419 locked_by_user_id = lock_info[0]
2420 2420 if user.user_id == locked_by_user_id:
2421 2421 log.debug(
2422 2422 'Got `push` action from user %s, now unlocking', user)
2423 2423 # unlock if we have push from user who locked
2424 2424 make_lock = False
2425 2425 else:
2426 2426 # we're not the same user who locked, ban with
2427 2427 # code defined in settings (default is 423 HTTP Locked) !
2428 2428 log.debug('Repo %s is currently locked by %s', repo, user)
2429 2429 currently_locked = True
2430 2430 elif action == 'pull':
2431 2431 # [0] user [1] date
2432 2432 if lock_info[0] and lock_info[1]:
2433 2433 log.debug('Repo %s is currently locked by %s', repo, user)
2434 2434 currently_locked = True
2435 2435 else:
2436 2436 log.debug('Setting lock on repo %s by %s', repo, user)
2437 2437 make_lock = True
2438 2438
2439 2439 else:
2440 2440 log.debug('Repository %s do not have locking enabled', repo)
2441 2441
2442 2442 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2443 2443 make_lock, currently_locked, lock_info)
2444 2444
2445 2445 from rhodecode.lib.auth import HasRepoPermissionAny
2446 2446 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2447 2447 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2448 2448 # if we don't have at least write permission we cannot make a lock
2449 2449 log.debug('lock state reset back to FALSE due to lack '
2450 2450 'of at least read permission')
2451 2451 make_lock = False
2452 2452
2453 2453 return make_lock, currently_locked, lock_info
2454 2454
2455 2455 @property
2456 2456 def last_commit_cache_update_diff(self):
2457 2457 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2458 2458
2459 2459 @classmethod
2460 2460 def _load_commit_change(cls, last_commit_cache):
2461 2461 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2462 2462 empty_date = datetime.datetime.fromtimestamp(0)
2463 2463 date_latest = last_commit_cache.get('date', empty_date)
2464 2464 try:
2465 2465 return parse_datetime(date_latest)
2466 2466 except Exception:
2467 2467 return empty_date
2468 2468
2469 2469 @property
2470 2470 def last_commit_change(self):
2471 2471 return self._load_commit_change(self.changeset_cache)
2472 2472
2473 2473 @property
2474 2474 def last_db_change(self):
2475 2475 return self.updated_on
2476 2476
2477 2477 @property
2478 2478 def clone_uri_hidden(self):
2479 2479 clone_uri = self.clone_uri
2480 2480 if clone_uri:
2481 2481 import urlobject
2482 2482 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2483 2483 if url_obj.password:
2484 2484 clone_uri = url_obj.with_password('*****')
2485 2485 return clone_uri
2486 2486
2487 2487 @property
2488 2488 def push_uri_hidden(self):
2489 2489 push_uri = self.push_uri
2490 2490 if push_uri:
2491 2491 import urlobject
2492 2492 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2493 2493 if url_obj.password:
2494 2494 push_uri = url_obj.with_password('*****')
2495 2495 return push_uri
2496 2496
2497 2497 def clone_url(self, **override):
2498 2498 from rhodecode.model.settings import SettingsModel
2499 2499
2500 2500 uri_tmpl = None
2501 2501 if 'with_id' in override:
2502 2502 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2503 2503 del override['with_id']
2504 2504
2505 2505 if 'uri_tmpl' in override:
2506 2506 uri_tmpl = override['uri_tmpl']
2507 2507 del override['uri_tmpl']
2508 2508
2509 2509 ssh = False
2510 2510 if 'ssh' in override:
2511 2511 ssh = True
2512 2512 del override['ssh']
2513 2513
2514 2514 # we didn't override our tmpl from **overrides
2515 2515 request = get_current_request()
2516 2516 if not uri_tmpl:
2517 2517 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2518 2518 rc_config = request.call_context.rc_config
2519 2519 else:
2520 2520 rc_config = SettingsModel().get_all_settings(cache=True)
2521 2521
2522 2522 if ssh:
2523 2523 uri_tmpl = rc_config.get(
2524 2524 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2525 2525
2526 2526 else:
2527 2527 uri_tmpl = rc_config.get(
2528 2528 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2529 2529
2530 2530 return get_clone_url(request=request,
2531 2531 uri_tmpl=uri_tmpl,
2532 2532 repo_name=self.repo_name,
2533 2533 repo_id=self.repo_id,
2534 2534 repo_type=self.repo_type,
2535 2535 **override)
2536 2536
2537 2537 def set_state(self, state):
2538 2538 self.repo_state = state
2539 2539 Session().add(self)
2540 2540 #==========================================================================
2541 2541 # SCM PROPERTIES
2542 2542 #==========================================================================
2543 2543
2544 2544 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False, reference_obj=None):
2545 2545 return get_commit_safe(
2546 2546 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2547 2547 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
2548 2548
2549 2549 def get_changeset(self, rev=None, pre_load=None):
2550 2550 warnings.warn("Use get_commit", DeprecationWarning)
2551 2551 commit_id = None
2552 2552 commit_idx = None
2553 2553 if isinstance(rev, str):
2554 2554 commit_id = rev
2555 2555 else:
2556 2556 commit_idx = rev
2557 2557 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2558 2558 pre_load=pre_load)
2559 2559
2560 2560 def get_landing_commit(self):
2561 2561 """
2562 2562 Returns landing commit, or if that doesn't exist returns the tip
2563 2563 """
2564 2564 _rev_type, _rev = self.landing_rev
2565 2565 commit = self.get_commit(_rev)
2566 2566 if isinstance(commit, EmptyCommit):
2567 2567 return self.get_commit()
2568 2568 return commit
2569 2569
2570 2570 def flush_commit_cache(self):
2571 self.update_commit_cache(cs_cache={'raw_id':'0'})
2571 self.update_commit_cache(cs_cache={'raw_id': '0'})
2572 2572 self.update_commit_cache()
2573 2573
2574 def update_commit_cache(self, cs_cache=None, config=None):
2574 def update_commit_cache(self, cs_cache=None, config=None, recursive=True):
2575 2575 """
2576 2576 Update cache of last commit for repository
2577 2577 cache_keys should be::
2578 2578
2579 2579 source_repo_id
2580 2580 short_id
2581 2581 raw_id
2582 2582 revision
2583 2583 parents
2584 2584 message
2585 2585 date
2586 2586 author
2587 2587 updated_on
2588 2588
2589 2589 """
2590 2590 from rhodecode.lib.vcs.backends.base import BaseCommit
2591 2591 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2592 2592 empty_date = datetime.datetime.fromtimestamp(0)
2593 2593 repo_commit_count = 0
2594 2594
2595 2595 if cs_cache is None:
2596 2596 # use no-cache version here
2597 2597 try:
2598 2598 scm_repo = self.scm_instance(cache=False, config=config)
2599 2599 except VCSError:
2600 2600 scm_repo = None
2601 2601 empty = scm_repo is None or scm_repo.is_empty()
2602 2602
2603 2603 if not empty:
2604 2604 cs_cache = scm_repo.get_commit(
2605 2605 pre_load=["author", "date", "message", "parents", "branch"])
2606 2606 repo_commit_count = scm_repo.count()
2607 2607 else:
2608 2608 cs_cache = EmptyCommit()
2609 2609
2610 2610 if isinstance(cs_cache, BaseCommit):
2611 2611 cs_cache = cs_cache.__json__()
2612 2612
2613 def maybe_update_recursive(instance, _config, _recursive, _cs_cache, _last_change):
2614 if _recursive:
2615 repo_id = instance.repo_id
2616 _cs_cache['source_repo_id'] = repo_id
2617 for gr in instance.groups_with_parents:
2618 gr.changeset_cache = _cs_cache
2619 gr.updated_on = _last_change
2620
2613 2621 def is_outdated(new_cs_cache):
2614 2622 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2615 2623 new_cs_cache['revision'] != self.changeset_cache['revision']):
2616 2624 return True
2617 2625 return False
2618 2626
2619 2627 # check if we have maybe already latest cached revision
2620 2628 if is_outdated(cs_cache) or not self.changeset_cache:
2621 2629 _current_datetime = datetime.datetime.utcnow()
2622 2630 last_change = cs_cache.get('date') or _current_datetime
2623 2631 # we check if last update is newer than the new value
2624 2632 # if yes, we use the current timestamp instead. Imagine you get
2625 2633 # old commit pushed 1y ago, we'd set last update 1y to ago.
2626 2634 last_change_timestamp = datetime_to_time(last_change)
2627 2635 current_timestamp = datetime_to_time(last_change)
2628 2636 if last_change_timestamp > current_timestamp and not empty:
2629 2637 cs_cache['date'] = _current_datetime
2630 2638
2631 2639 # also store size of repo
2632 2640 cs_cache['repo_commit_count'] = repo_commit_count
2633 2641
2634 2642 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2635 2643 cs_cache['updated_on'] = time.time()
2636 2644 self.changeset_cache = cs_cache
2637 2645 self.updated_on = last_change
2638 2646 Session().add(self)
2647 maybe_update_recursive(self, config, recursive, cs_cache, last_change)
2639 2648 Session().commit()
2640 2649
2641 2650 else:
2642 2651 if empty:
2643 2652 cs_cache = EmptyCommit().__json__()
2644 2653 else:
2645 2654 cs_cache = self.changeset_cache
2646 2655
2647 2656 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2648 2657
2649 2658 cs_cache['updated_on'] = time.time()
2650 2659 self.changeset_cache = cs_cache
2651 2660 self.updated_on = _date_latest
2652 2661 Session().add(self)
2662 maybe_update_recursive(self, config, recursive, cs_cache, _date_latest)
2653 2663 Session().commit()
2654 2664
2655 2665 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2656 2666 self.repo_name, cs_cache, _date_latest)
2657 2667
2658 2668 @property
2659 2669 def tip(self):
2660 2670 return self.get_commit('tip')
2661 2671
2662 2672 @property
2663 2673 def author(self):
2664 2674 return self.tip.author
2665 2675
2666 2676 @property
2667 2677 def last_change(self):
2668 2678 return self.scm_instance().last_change
2669 2679
2670 2680 def get_comments(self, revisions=None):
2671 2681 """
2672 2682 Returns comments for this repository grouped by revisions
2673 2683
2674 2684 :param revisions: filter query by revisions only
2675 2685 """
2676 2686 cmts = ChangesetComment.query()\
2677 2687 .filter(ChangesetComment.repo == self)
2678 2688 if revisions:
2679 2689 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2680 2690 grouped = collections.defaultdict(list)
2681 2691 for cmt in cmts.all():
2682 2692 grouped[cmt.revision].append(cmt)
2683 2693 return grouped
2684 2694
2685 2695 def statuses(self, revisions=None):
2686 2696 """
2687 2697 Returns statuses for this repository
2688 2698
2689 2699 :param revisions: list of revisions to get statuses for
2690 2700 """
2691 2701 statuses = ChangesetStatus.query()\
2692 2702 .filter(ChangesetStatus.repo == self)\
2693 2703 .filter(ChangesetStatus.version == 0)
2694 2704
2695 2705 if revisions:
2696 2706 # Try doing the filtering in chunks to avoid hitting limits
2697 2707 size = 500
2698 2708 status_results = []
2699 2709 for chunk in range(0, len(revisions), size):
2700 2710 status_results += statuses.filter(
2701 2711 ChangesetStatus.revision.in_(
2702 2712 revisions[chunk: chunk+size])
2703 2713 ).all()
2704 2714 else:
2705 2715 status_results = statuses.all()
2706 2716
2707 2717 grouped = {}
2708 2718
2709 2719 # maybe we have open new pullrequest without a status?
2710 2720 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2711 2721 status_lbl = ChangesetStatus.get_status_lbl(stat)
2712 2722 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2713 2723 for rev in pr.revisions:
2714 2724 pr_id = pr.pull_request_id
2715 2725 pr_repo = pr.target_repo.repo_name
2716 2726 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2717 2727
2718 2728 for stat in status_results:
2719 2729 pr_id = pr_repo = None
2720 2730 if stat.pull_request:
2721 2731 pr_id = stat.pull_request.pull_request_id
2722 2732 pr_repo = stat.pull_request.target_repo.repo_name
2723 2733 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2724 2734 pr_id, pr_repo]
2725 2735 return grouped
2726 2736
2727 2737 # ==========================================================================
2728 2738 # SCM CACHE INSTANCE
2729 2739 # ==========================================================================
2730 2740
2731 2741 def scm_instance(self, **kwargs):
2732 2742 import rhodecode
2733 2743
2734 2744 # Passing a config will not hit the cache currently only used
2735 2745 # for repo2dbmapper
2736 2746 config = kwargs.pop('config', None)
2737 2747 cache = kwargs.pop('cache', None)
2738 2748 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2739 2749 if vcs_full_cache is not None:
2740 2750 # allows override global config
2741 2751 full_cache = vcs_full_cache
2742 2752 else:
2743 2753 full_cache = rhodecode.ConfigGet().get_bool('vcs_full_cache')
2744 2754 # if cache is NOT defined use default global, else we have a full
2745 2755 # control over cache behaviour
2746 2756 if cache is None and full_cache and not config:
2747 2757 log.debug('Initializing pure cached instance for %s', self.repo_path)
2748 2758 return self._get_instance_cached()
2749 2759
2750 2760 # cache here is sent to the "vcs server"
2751 2761 return self._get_instance(cache=bool(cache), config=config)
2752 2762
2753 2763 def _get_instance_cached(self):
2754 2764 from rhodecode.lib import rc_cache
2755 2765
2756 2766 cache_namespace_uid = f'repo_instance.{self.repo_id}'
2757 2767 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2758 2768
2759 2769 # we must use thread scoped cache here,
2760 2770 # because each thread of gevent needs it's own not shared connection and cache
2761 2771 # we also alter `args` so the cache key is individual for every green thread.
2762 2772 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2763 2773 inv_context_manager = rc_cache.InvalidationContext(key=repo_namespace_key, thread_scoped=True)
2764 2774
2765 2775 # our wrapped caching function that takes state_uid to save the previous state in
2766 2776 def cache_generator(_state_uid):
2767 2777
2768 2778 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2769 2779 def get_instance_cached(_repo_id, _process_context_id):
2770 2780 # we save in cached func the generation state so we can detect a change and invalidate caches
2771 2781 return _state_uid, self._get_instance(repo_state_uid=_state_uid)
2772 2782
2773 2783 return get_instance_cached
2774 2784
2775 2785 with inv_context_manager as invalidation_context:
2776 2786 cache_state_uid = invalidation_context.state_uid
2777 2787 cache_func = cache_generator(cache_state_uid)
2778 2788
2779 2789 args = self.repo_id, inv_context_manager.proc_key
2780 2790
2781 2791 previous_state_uid, instance = cache_func(*args)
2782 2792
2783 2793 # now compare keys, the "cache" state vs expected state.
2784 2794 if previous_state_uid != cache_state_uid:
2785 2795 log.warning('Cached state uid %s is different than current state uid %s',
2786 2796 previous_state_uid, cache_state_uid)
2787 2797 _, instance = cache_func.refresh(*args)
2788 2798
2789 2799 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2790 2800 return instance
2791 2801
2792 2802 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2793 2803 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2794 2804 self.repo_type, self.repo_path, cache)
2795 2805 config = config or self._config
2796 2806 custom_wire = {
2797 2807 'cache': cache, # controls the vcs.remote cache
2798 2808 'repo_state_uid': repo_state_uid
2799 2809 }
2800 2810
2801 2811 repo = get_vcs_instance(
2802 2812 repo_path=safe_str(self.repo_full_path),
2803 2813 config=config,
2804 2814 with_wire=custom_wire,
2805 2815 create=False,
2806 2816 _vcs_alias=self.repo_type)
2807 2817 if repo is not None:
2808 2818 repo.count() # cache rebuild
2809 2819
2810 2820 return repo
2811 2821
2812 2822 def get_shadow_repository_path(self, workspace_id):
2813 2823 from rhodecode.lib.vcs.backends.base import BaseRepository
2814 2824 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2815 2825 self.repo_full_path, self.repo_id, workspace_id)
2816 2826 return shadow_repo_path
2817 2827
2818 2828 def __json__(self):
2819 2829 return {'landing_rev': self.landing_rev}
2820 2830
2821 2831 def get_dict(self):
2822 2832
2823 2833 # Since we transformed `repo_name` to a hybrid property, we need to
2824 2834 # keep compatibility with the code which uses `repo_name` field.
2825 2835
2826 2836 result = super(Repository, self).get_dict()
2827 2837 result['repo_name'] = result.pop('_repo_name', None)
2828 2838 result.pop('_changeset_cache', '')
2829 2839 return result
2830 2840
2831 2841
2832 2842 class RepoGroup(Base, BaseModel):
2833 2843 __tablename__ = 'groups'
2834 2844 __table_args__ = (
2835 2845 UniqueConstraint('group_name', 'group_parent_id'),
2836 2846 base_table_args,
2837 2847 )
2838 2848
2839 2849 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2840 2850
2841 2851 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2842 2852 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2843 2853 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2844 2854 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2845 2855 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2846 2856 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2847 2857 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2848 2858 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2849 2859 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2850 2860 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2851 2861 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2852 2862
2853 2863 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id', back_populates='group')
2854 2864 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='group')
2855 2865 parent_group = relationship('RepoGroup', remote_side=group_id)
2856 2866 user = relationship('User', back_populates='repository_groups')
2857 2867 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo_group')
2858 2868
2859 2869 # no cascade, set NULL
2860 2870 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id', viewonly=True)
2861 2871
2862 2872 def __init__(self, group_name='', parent_group=None):
2863 2873 self.group_name = group_name
2864 2874 self.parent_group = parent_group
2865 2875
2866 2876 def __repr__(self):
2867 2877 return f"<{self.cls_name}('id:{self.group_id}:{self.group_name}')>"
2868 2878
2869 2879 @hybrid_property
2870 2880 def group_name(self):
2871 2881 return self._group_name
2872 2882
2873 2883 @group_name.setter
2874 2884 def group_name(self, value):
2875 2885 self._group_name = value
2876 2886 self.group_name_hash = self.hash_repo_group_name(value)
2877 2887
2878 2888 @classmethod
2879 2889 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2880 2890 from rhodecode.lib.vcs.backends.base import EmptyCommit
2881 2891 dummy = EmptyCommit().__json__()
2882 2892 if not changeset_cache_raw:
2883 2893 dummy['source_repo_id'] = repo_id
2884 2894 return json.loads(json.dumps(dummy))
2885 2895
2886 2896 try:
2887 2897 return json.loads(changeset_cache_raw)
2888 2898 except TypeError:
2889 2899 return dummy
2890 2900 except Exception:
2891 2901 log.error(traceback.format_exc())
2892 2902 return dummy
2893 2903
2894 2904 @hybrid_property
2895 2905 def changeset_cache(self):
2896 2906 return self._load_changeset_cache('', self._changeset_cache)
2897 2907
2898 2908 @changeset_cache.setter
2899 2909 def changeset_cache(self, val):
2900 2910 try:
2901 2911 self._changeset_cache = json.dumps(val)
2902 2912 except Exception:
2903 2913 log.error(traceback.format_exc())
2904 2914
2905 2915 @validates('group_parent_id')
2906 2916 def validate_group_parent_id(self, key, val):
2907 2917 """
2908 2918 Check cycle references for a parent group to self
2909 2919 """
2910 2920 if self.group_id and val:
2911 2921 assert val != self.group_id
2912 2922
2913 2923 return val
2914 2924
2915 2925 @hybrid_property
2916 2926 def description_safe(self):
2917 2927 return description_escaper(self.group_description)
2918 2928
2919 2929 @classmethod
2920 2930 def hash_repo_group_name(cls, repo_group_name):
2921 2931 val = remove_formatting(repo_group_name)
2922 2932 val = safe_str(val).lower()
2923 2933 chars = []
2924 2934 for c in val:
2925 2935 if c not in string.ascii_letters:
2926 2936 c = str(ord(c))
2927 2937 chars.append(c)
2928 2938
2929 2939 return ''.join(chars)
2930 2940
2931 2941 @classmethod
2932 2942 def _generate_choice(cls, repo_group):
2933 2943 from webhelpers2.html import literal as _literal
2934 2944
2935 2945 def _name(k):
2936 2946 return _literal(cls.CHOICES_SEPARATOR.join(k))
2937 2947
2938 2948 return repo_group.group_id, _name(repo_group.full_path_splitted)
2939 2949
2940 2950 @classmethod
2941 2951 def groups_choices(cls, groups=None, show_empty_group=True):
2942 2952 if not groups:
2943 2953 groups = cls.query().all()
2944 2954
2945 2955 repo_groups = []
2946 2956 if show_empty_group:
2947 2957 repo_groups = [(-1, '-- %s --' % _('No parent'))]
2948 2958
2949 2959 repo_groups.extend([cls._generate_choice(x) for x in groups])
2950 2960
2951 2961 repo_groups = sorted(
2952 2962 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2953 2963 return repo_groups
2954 2964
2955 2965 @classmethod
2956 2966 def url_sep(cls):
2957 2967 return URL_SEP
2958 2968
2959 2969 @classmethod
2960 2970 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2961 2971 if case_insensitive:
2962 2972 gr = cls.query().filter(func.lower(cls.group_name)
2963 2973 == func.lower(group_name))
2964 2974 else:
2965 2975 gr = cls.query().filter(cls.group_name == group_name)
2966 2976 if cache:
2967 2977 name_key = _hash_key(group_name)
2968 2978 gr = gr.options(
2969 2979 FromCache("sql_cache_short", f"get_group_{name_key}"))
2970 2980 return gr.scalar()
2971 2981
2972 2982 @classmethod
2973 2983 def get_user_personal_repo_group(cls, user_id):
2974 2984 user = User.get(user_id)
2975 2985 if user.username == User.DEFAULT_USER:
2976 2986 return None
2977 2987
2978 2988 return cls.query()\
2979 2989 .filter(cls.personal == true()) \
2980 2990 .filter(cls.user == user) \
2981 2991 .order_by(cls.group_id.asc()) \
2982 2992 .first()
2983 2993
2984 2994 @classmethod
2985 2995 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2986 2996 case_insensitive=True):
2987 2997 q = RepoGroup.query()
2988 2998
2989 2999 if not isinstance(user_id, Optional):
2990 3000 q = q.filter(RepoGroup.user_id == user_id)
2991 3001
2992 3002 if not isinstance(group_id, Optional):
2993 3003 q = q.filter(RepoGroup.group_parent_id == group_id)
2994 3004
2995 3005 if case_insensitive:
2996 3006 q = q.order_by(func.lower(RepoGroup.group_name))
2997 3007 else:
2998 3008 q = q.order_by(RepoGroup.group_name)
2999 3009 return q.all()
3000 3010
3001 3011 @property
3002 3012 def parents(self, parents_recursion_limit=10):
3003 3013 groups = []
3004 3014 if self.parent_group is None:
3005 3015 return groups
3006 3016 cur_gr = self.parent_group
3007 3017 groups.insert(0, cur_gr)
3008 3018 cnt = 0
3009 3019 while 1:
3010 3020 cnt += 1
3011 3021 gr = getattr(cur_gr, 'parent_group', None)
3012 3022 cur_gr = cur_gr.parent_group
3013 3023 if gr is None:
3014 3024 break
3015 3025 if cnt == parents_recursion_limit:
3016 3026 # this will prevent accidental infinit loops
3017 3027 log.error('more than %s parents found for group %s, stopping '
3018 3028 'recursive parent fetching', parents_recursion_limit, self)
3019 3029 break
3020 3030
3021 3031 groups.insert(0, gr)
3022 3032 return groups
3023 3033
3024 3034 @property
3025 3035 def last_commit_cache_update_diff(self):
3026 3036 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
3027 3037
3028 3038 @classmethod
3029 3039 def _load_commit_change(cls, last_commit_cache):
3030 3040 from rhodecode.lib.vcs.utils.helpers import parse_datetime
3031 3041 empty_date = datetime.datetime.fromtimestamp(0)
3032 3042 date_latest = last_commit_cache.get('date', empty_date)
3033 3043 try:
3034 3044 return parse_datetime(date_latest)
3035 3045 except Exception:
3036 3046 return empty_date
3037 3047
3038 3048 @property
3039 3049 def last_commit_change(self):
3040 3050 return self._load_commit_change(self.changeset_cache)
3041 3051
3042 3052 @property
3043 3053 def last_db_change(self):
3044 3054 return self.updated_on
3045 3055
3046 3056 @property
3047 3057 def children(self):
3048 3058 return RepoGroup.query().filter(RepoGroup.parent_group == self)
3049 3059
3050 3060 @property
3051 3061 def name(self):
3052 3062 return self.group_name.split(RepoGroup.url_sep())[-1]
3053 3063
3054 3064 @property
3055 3065 def full_path(self):
3056 3066 return self.group_name
3057 3067
3058 3068 @property
3059 3069 def full_path_splitted(self):
3060 3070 return self.group_name.split(RepoGroup.url_sep())
3061 3071
3062 3072 @property
3063 3073 def repositories(self):
3064 3074 return Repository.query()\
3065 3075 .filter(Repository.group == self)\
3066 3076 .order_by(Repository.repo_name)
3067 3077
3068 3078 @property
3069 3079 def repositories_recursive_count(self):
3070 3080 cnt = self.repositories.count()
3071 3081
3072 3082 def children_count(group):
3073 3083 cnt = 0
3074 3084 for child in group.children:
3075 3085 cnt += child.repositories.count()
3076 3086 cnt += children_count(child)
3077 3087 return cnt
3078 3088
3079 3089 return cnt + children_count(self)
3080 3090
3081 3091 def _recursive_objects(self, include_repos=True, include_groups=True):
3082 3092 all_ = []
3083 3093
3084 3094 def _get_members(root_gr):
3085 3095 if include_repos:
3086 3096 for r in root_gr.repositories:
3087 3097 all_.append(r)
3088 3098 childs = root_gr.children.all()
3089 3099 if childs:
3090 3100 for gr in childs:
3091 3101 if include_groups:
3092 3102 all_.append(gr)
3093 3103 _get_members(gr)
3094 3104
3095 3105 root_group = []
3096 3106 if include_groups:
3097 3107 root_group = [self]
3098 3108
3099 3109 _get_members(self)
3100 3110 return root_group + all_
3101 3111
3102 3112 def recursive_groups_and_repos(self):
3103 3113 """
3104 3114 Recursive return all groups, with repositories in those groups
3105 3115 """
3106 3116 return self._recursive_objects()
3107 3117
3108 3118 def recursive_groups(self):
3109 3119 """
3110 3120 Returns all children groups for this group including children of children
3111 3121 """
3112 3122 return self._recursive_objects(include_repos=False)
3113 3123
3114 3124 def recursive_repos(self):
3115 3125 """
3116 3126 Returns all children repositories for this group
3117 3127 """
3118 3128 return self._recursive_objects(include_groups=False)
3119 3129
3120 3130 def get_new_name(self, group_name):
3121 3131 """
3122 3132 returns new full group name based on parent and new name
3123 3133
3124 3134 :param group_name:
3125 3135 """
3126 3136 path_prefix = (self.parent_group.full_path_splitted if
3127 3137 self.parent_group else [])
3128 3138 return RepoGroup.url_sep().join(path_prefix + [group_name])
3129 3139
3130 3140 def update_commit_cache(self, config=None):
3131 3141 """
3132 3142 Update cache of last commit for newest repository inside this repository group.
3133 3143 cache_keys should be::
3134 3144
3135 3145 source_repo_id
3136 3146 short_id
3137 3147 raw_id
3138 3148 revision
3139 3149 parents
3140 3150 message
3141 3151 date
3142 3152 author
3143 3153
3144 3154 """
3145 3155 from rhodecode.lib.vcs.utils.helpers import parse_datetime
3146 3156 empty_date = datetime.datetime.fromtimestamp(0)
3147 3157
3148 3158 def repo_groups_and_repos(root_gr):
3149 3159 for _repo in root_gr.repositories:
3150 3160 yield _repo
3151 3161 for child_group in root_gr.children.all():
3152 3162 yield child_group
3153 3163
3154 3164 latest_repo_cs_cache = {}
3155 3165 for obj in repo_groups_and_repos(self):
3156 3166 repo_cs_cache = obj.changeset_cache
3157 3167 date_latest = latest_repo_cs_cache.get('date', empty_date)
3158 3168 date_current = repo_cs_cache.get('date', empty_date)
3159 3169 current_timestamp = datetime_to_time(parse_datetime(date_latest))
3160 3170 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
3161 3171 latest_repo_cs_cache = repo_cs_cache
3162 3172 if hasattr(obj, 'repo_id'):
3163 3173 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
3164 3174 else:
3165 3175 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
3166 3176
3167 3177 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
3168 3178
3169 3179 latest_repo_cs_cache['updated_on'] = time.time()
3170 3180 self.changeset_cache = latest_repo_cs_cache
3171 3181 self.updated_on = _date_latest
3172 3182 Session().add(self)
3173 3183 Session().commit()
3174 3184
3175 3185 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
3176 3186 self.group_name, latest_repo_cs_cache, _date_latest)
3177 3187
3178 3188 def permissions(self, with_admins=True, with_owner=True,
3179 3189 expand_from_user_groups=False):
3180 3190 """
3181 3191 Permissions for repository groups
3182 3192 """
3183 3193 _admin_perm = 'group.admin'
3184 3194
3185 3195 owner_row = []
3186 3196 if with_owner:
3187 3197 usr = AttributeDict(self.user.get_dict())
3188 3198 usr.owner_row = True
3189 3199 usr.permission = _admin_perm
3190 3200 owner_row.append(usr)
3191 3201
3192 3202 super_admin_ids = []
3193 3203 super_admin_rows = []
3194 3204 if with_admins:
3195 3205 for usr in User.get_all_super_admins():
3196 3206 super_admin_ids.append(usr.user_id)
3197 3207 # if this admin is also owner, don't double the record
3198 3208 if usr.user_id == owner_row[0].user_id:
3199 3209 owner_row[0].admin_row = True
3200 3210 else:
3201 3211 usr = AttributeDict(usr.get_dict())
3202 3212 usr.admin_row = True
3203 3213 usr.permission = _admin_perm
3204 3214 super_admin_rows.append(usr)
3205 3215
3206 3216 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3207 3217 q = q.options(joinedload(UserRepoGroupToPerm.group),
3208 3218 joinedload(UserRepoGroupToPerm.user),
3209 3219 joinedload(UserRepoGroupToPerm.permission),)
3210 3220
3211 3221 # get owners and admins and permissions. We do a trick of re-writing
3212 3222 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3213 3223 # has a global reference and changing one object propagates to all
3214 3224 # others. This means if admin is also an owner admin_row that change
3215 3225 # would propagate to both objects
3216 3226 perm_rows = []
3217 3227 for _usr in q.all():
3218 3228 usr = AttributeDict(_usr.user.get_dict())
3219 3229 # if this user is also owner/admin, mark as duplicate record
3220 3230 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3221 3231 usr.duplicate_perm = True
3222 3232 usr.permission = _usr.permission.permission_name
3223 3233 perm_rows.append(usr)
3224 3234
3225 3235 # filter the perm rows by 'default' first and then sort them by
3226 3236 # admin,write,read,none permissions sorted again alphabetically in
3227 3237 # each group
3228 3238 perm_rows = sorted(perm_rows, key=display_user_sort)
3229 3239
3230 3240 user_groups_rows = []
3231 3241 if expand_from_user_groups:
3232 3242 for ug in self.permission_user_groups(with_members=True):
3233 3243 for user_data in ug.members:
3234 3244 user_groups_rows.append(user_data)
3235 3245
3236 3246 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3237 3247
3238 3248 def permission_user_groups(self, with_members=False):
3239 3249 q = UserGroupRepoGroupToPerm.query()\
3240 3250 .filter(UserGroupRepoGroupToPerm.group == self)
3241 3251 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3242 3252 joinedload(UserGroupRepoGroupToPerm.users_group),
3243 3253 joinedload(UserGroupRepoGroupToPerm.permission),)
3244 3254
3245 3255 perm_rows = []
3246 3256 for _user_group in q.all():
3247 3257 entry = AttributeDict(_user_group.users_group.get_dict())
3248 3258 entry.permission = _user_group.permission.permission_name
3249 3259 if with_members:
3250 3260 entry.members = [x.user.get_dict()
3251 3261 for x in _user_group.users_group.members]
3252 3262 perm_rows.append(entry)
3253 3263
3254 3264 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3255 3265 return perm_rows
3256 3266
3257 3267 def get_api_data(self):
3258 3268 """
3259 3269 Common function for generating api data
3260 3270
3261 3271 """
3262 3272 group = self
3263 3273 data = {
3264 3274 'group_id': group.group_id,
3265 3275 'group_name': group.group_name,
3266 3276 'group_description': group.description_safe,
3267 3277 'parent_group': group.parent_group.group_name if group.parent_group else None,
3268 3278 'repositories': [x.repo_name for x in group.repositories],
3269 3279 'owner': group.user.username,
3270 3280 }
3271 3281 return data
3272 3282
3273 3283 def get_dict(self):
3274 3284 # Since we transformed `group_name` to a hybrid property, we need to
3275 3285 # keep compatibility with the code which uses `group_name` field.
3276 3286 result = super(RepoGroup, self).get_dict()
3277 3287 result['group_name'] = result.pop('_group_name', None)
3278 3288 result.pop('_changeset_cache', '')
3279 3289 return result
3280 3290
3281 3291
3282 3292 class Permission(Base, BaseModel):
3283 3293 __tablename__ = 'permissions'
3284 3294 __table_args__ = (
3285 3295 Index('p_perm_name_idx', 'permission_name'),
3286 3296 base_table_args,
3287 3297 )
3288 3298
3289 3299 PERMS = [
3290 3300 ('hg.admin', _('RhodeCode Super Administrator')),
3291 3301
3292 3302 ('repository.none', _('Repository no access')),
3293 3303 ('repository.read', _('Repository read access')),
3294 3304 ('repository.write', _('Repository write access')),
3295 3305 ('repository.admin', _('Repository admin access')),
3296 3306
3297 3307 ('group.none', _('Repository group no access')),
3298 3308 ('group.read', _('Repository group read access')),
3299 3309 ('group.write', _('Repository group write access')),
3300 3310 ('group.admin', _('Repository group admin access')),
3301 3311
3302 3312 ('usergroup.none', _('User group no access')),
3303 3313 ('usergroup.read', _('User group read access')),
3304 3314 ('usergroup.write', _('User group write access')),
3305 3315 ('usergroup.admin', _('User group admin access')),
3306 3316
3307 3317 ('branch.none', _('Branch no permissions')),
3308 3318 ('branch.merge', _('Branch access by web merge')),
3309 3319 ('branch.push', _('Branch access by push')),
3310 3320 ('branch.push_force', _('Branch access by push with force')),
3311 3321
3312 3322 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3313 3323 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3314 3324
3315 3325 ('hg.usergroup.create.false', _('User Group creation disabled')),
3316 3326 ('hg.usergroup.create.true', _('User Group creation enabled')),
3317 3327
3318 3328 ('hg.create.none', _('Repository creation disabled')),
3319 3329 ('hg.create.repository', _('Repository creation enabled')),
3320 3330 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3321 3331 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3322 3332
3323 3333 ('hg.fork.none', _('Repository forking disabled')),
3324 3334 ('hg.fork.repository', _('Repository forking enabled')),
3325 3335
3326 3336 ('hg.register.none', _('Registration disabled')),
3327 3337 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3328 3338 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3329 3339
3330 3340 ('hg.password_reset.enabled', _('Password reset enabled')),
3331 3341 ('hg.password_reset.hidden', _('Password reset hidden')),
3332 3342 ('hg.password_reset.disabled', _('Password reset disabled')),
3333 3343
3334 3344 ('hg.extern_activate.manual', _('Manual activation of external account')),
3335 3345 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3336 3346
3337 3347 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3338 3348 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3339 3349 ]
3340 3350
3341 3351 # definition of system default permissions for DEFAULT user, created on
3342 3352 # system setup
3343 3353 DEFAULT_USER_PERMISSIONS = [
3344 3354 # object perms
3345 3355 'repository.read',
3346 3356 'group.read',
3347 3357 'usergroup.read',
3348 3358 # branch, for backward compat we need same value as before so forced pushed
3349 3359 'branch.push_force',
3350 3360 # global
3351 3361 'hg.create.repository',
3352 3362 'hg.repogroup.create.false',
3353 3363 'hg.usergroup.create.false',
3354 3364 'hg.create.write_on_repogroup.true',
3355 3365 'hg.fork.repository',
3356 3366 'hg.register.manual_activate',
3357 3367 'hg.password_reset.enabled',
3358 3368 'hg.extern_activate.auto',
3359 3369 'hg.inherit_default_perms.true',
3360 3370 ]
3361 3371
3362 3372 # defines which permissions are more important higher the more important
3363 3373 # Weight defines which permissions are more important.
3364 3374 # The higher number the more important.
3365 3375 PERM_WEIGHTS = {
3366 3376 'repository.none': 0,
3367 3377 'repository.read': 1,
3368 3378 'repository.write': 3,
3369 3379 'repository.admin': 4,
3370 3380
3371 3381 'group.none': 0,
3372 3382 'group.read': 1,
3373 3383 'group.write': 3,
3374 3384 'group.admin': 4,
3375 3385
3376 3386 'usergroup.none': 0,
3377 3387 'usergroup.read': 1,
3378 3388 'usergroup.write': 3,
3379 3389 'usergroup.admin': 4,
3380 3390
3381 3391 'branch.none': 0,
3382 3392 'branch.merge': 1,
3383 3393 'branch.push': 3,
3384 3394 'branch.push_force': 4,
3385 3395
3386 3396 'hg.repogroup.create.false': 0,
3387 3397 'hg.repogroup.create.true': 1,
3388 3398
3389 3399 'hg.usergroup.create.false': 0,
3390 3400 'hg.usergroup.create.true': 1,
3391 3401
3392 3402 'hg.fork.none': 0,
3393 3403 'hg.fork.repository': 1,
3394 3404 'hg.create.none': 0,
3395 3405 'hg.create.repository': 1
3396 3406 }
3397 3407
3398 3408 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3399 3409 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3400 3410 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3401 3411
3402 3412 def __repr__(self):
3403 3413 return "<%s('%s:%s')>" % (
3404 3414 self.cls_name, self.permission_id, self.permission_name
3405 3415 )
3406 3416
3407 3417 @classmethod
3408 3418 def get_by_key(cls, key):
3409 3419 return cls.query().filter(cls.permission_name == key).scalar()
3410 3420
3411 3421 @classmethod
3412 3422 def get_default_repo_perms(cls, user_id, repo_id=None):
3413 3423 q = Session().query(UserRepoToPerm, Repository, Permission)\
3414 3424 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3415 3425 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3416 3426 .filter(UserRepoToPerm.user_id == user_id)
3417 3427 if repo_id:
3418 3428 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3419 3429 return q.all()
3420 3430
3421 3431 @classmethod
3422 3432 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3423 3433 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3424 3434 .join(
3425 3435 Permission,
3426 3436 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3427 3437 .join(
3428 3438 UserRepoToPerm,
3429 3439 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3430 3440 .filter(UserRepoToPerm.user_id == user_id)
3431 3441
3432 3442 if repo_id:
3433 3443 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3434 3444 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3435 3445
3436 3446 @classmethod
3437 3447 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3438 3448 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3439 3449 .join(
3440 3450 Permission,
3441 3451 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3442 3452 .join(
3443 3453 Repository,
3444 3454 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3445 3455 .join(
3446 3456 UserGroup,
3447 3457 UserGroupRepoToPerm.users_group_id ==
3448 3458 UserGroup.users_group_id)\
3449 3459 .join(
3450 3460 UserGroupMember,
3451 3461 UserGroupRepoToPerm.users_group_id ==
3452 3462 UserGroupMember.users_group_id)\
3453 3463 .filter(
3454 3464 UserGroupMember.user_id == user_id,
3455 3465 UserGroup.users_group_active == true())
3456 3466 if repo_id:
3457 3467 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3458 3468 return q.all()
3459 3469
3460 3470 @classmethod
3461 3471 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3462 3472 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3463 3473 .join(
3464 3474 Permission,
3465 3475 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3466 3476 .join(
3467 3477 UserGroupRepoToPerm,
3468 3478 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3469 3479 .join(
3470 3480 UserGroup,
3471 3481 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3472 3482 .join(
3473 3483 UserGroupMember,
3474 3484 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3475 3485 .filter(
3476 3486 UserGroupMember.user_id == user_id,
3477 3487 UserGroup.users_group_active == true())
3478 3488
3479 3489 if repo_id:
3480 3490 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3481 3491 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3482 3492
3483 3493 @classmethod
3484 3494 def get_default_group_perms(cls, user_id, repo_group_id=None):
3485 3495 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3486 3496 .join(
3487 3497 Permission,
3488 3498 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3489 3499 .join(
3490 3500 RepoGroup,
3491 3501 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3492 3502 .filter(UserRepoGroupToPerm.user_id == user_id)
3493 3503 if repo_group_id:
3494 3504 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3495 3505 return q.all()
3496 3506
3497 3507 @classmethod
3498 3508 def get_default_group_perms_from_user_group(
3499 3509 cls, user_id, repo_group_id=None):
3500 3510 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3501 3511 .join(
3502 3512 Permission,
3503 3513 UserGroupRepoGroupToPerm.permission_id ==
3504 3514 Permission.permission_id)\
3505 3515 .join(
3506 3516 RepoGroup,
3507 3517 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3508 3518 .join(
3509 3519 UserGroup,
3510 3520 UserGroupRepoGroupToPerm.users_group_id ==
3511 3521 UserGroup.users_group_id)\
3512 3522 .join(
3513 3523 UserGroupMember,
3514 3524 UserGroupRepoGroupToPerm.users_group_id ==
3515 3525 UserGroupMember.users_group_id)\
3516 3526 .filter(
3517 3527 UserGroupMember.user_id == user_id,
3518 3528 UserGroup.users_group_active == true())
3519 3529 if repo_group_id:
3520 3530 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3521 3531 return q.all()
3522 3532
3523 3533 @classmethod
3524 3534 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3525 3535 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3526 3536 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3527 3537 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3528 3538 .filter(UserUserGroupToPerm.user_id == user_id)
3529 3539 if user_group_id:
3530 3540 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3531 3541 return q.all()
3532 3542
3533 3543 @classmethod
3534 3544 def get_default_user_group_perms_from_user_group(
3535 3545 cls, user_id, user_group_id=None):
3536 3546 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3537 3547 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3538 3548 .join(
3539 3549 Permission,
3540 3550 UserGroupUserGroupToPerm.permission_id ==
3541 3551 Permission.permission_id)\
3542 3552 .join(
3543 3553 TargetUserGroup,
3544 3554 UserGroupUserGroupToPerm.target_user_group_id ==
3545 3555 TargetUserGroup.users_group_id)\
3546 3556 .join(
3547 3557 UserGroup,
3548 3558 UserGroupUserGroupToPerm.user_group_id ==
3549 3559 UserGroup.users_group_id)\
3550 3560 .join(
3551 3561 UserGroupMember,
3552 3562 UserGroupUserGroupToPerm.user_group_id ==
3553 3563 UserGroupMember.users_group_id)\
3554 3564 .filter(
3555 3565 UserGroupMember.user_id == user_id,
3556 3566 UserGroup.users_group_active == true())
3557 3567 if user_group_id:
3558 3568 q = q.filter(
3559 3569 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3560 3570
3561 3571 return q.all()
3562 3572
3563 3573
3564 3574 class UserRepoToPerm(Base, BaseModel):
3565 3575 __tablename__ = 'repo_to_perm'
3566 3576 __table_args__ = (
3567 3577 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3568 3578 base_table_args
3569 3579 )
3570 3580
3571 3581 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3572 3582 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3573 3583 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3574 3584 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3575 3585
3576 3586 user = relationship('User', back_populates="repo_to_perm")
3577 3587 repository = relationship('Repository', back_populates="repo_to_perm")
3578 3588 permission = relationship('Permission')
3579 3589
3580 3590 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined', back_populates='user_repo_to_perm')
3581 3591
3582 3592 @classmethod
3583 3593 def create(cls, user, repository, permission):
3584 3594 n = cls()
3585 3595 n.user = user
3586 3596 n.repository = repository
3587 3597 n.permission = permission
3588 3598 Session().add(n)
3589 3599 return n
3590 3600
3591 3601 def __repr__(self):
3592 3602 return f'<{self.user} => {self.repository} >'
3593 3603
3594 3604
3595 3605 class UserUserGroupToPerm(Base, BaseModel):
3596 3606 __tablename__ = 'user_user_group_to_perm'
3597 3607 __table_args__ = (
3598 3608 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3599 3609 base_table_args
3600 3610 )
3601 3611
3602 3612 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3603 3613 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3604 3614 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3605 3615 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3606 3616
3607 3617 user = relationship('User', back_populates='user_group_to_perm')
3608 3618 user_group = relationship('UserGroup', back_populates='user_user_group_to_perm')
3609 3619 permission = relationship('Permission')
3610 3620
3611 3621 @classmethod
3612 3622 def create(cls, user, user_group, permission):
3613 3623 n = cls()
3614 3624 n.user = user
3615 3625 n.user_group = user_group
3616 3626 n.permission = permission
3617 3627 Session().add(n)
3618 3628 return n
3619 3629
3620 3630 def __repr__(self):
3621 3631 return f'<{self.user} => {self.user_group} >'
3622 3632
3623 3633
3624 3634 class UserToPerm(Base, BaseModel):
3625 3635 __tablename__ = 'user_to_perm'
3626 3636 __table_args__ = (
3627 3637 UniqueConstraint('user_id', 'permission_id'),
3628 3638 base_table_args
3629 3639 )
3630 3640
3631 3641 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3632 3642 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3633 3643 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3634 3644
3635 3645 user = relationship('User', back_populates='user_perms')
3636 3646 permission = relationship('Permission', lazy='joined')
3637 3647
3638 3648 def __repr__(self):
3639 3649 return f'<{self.user} => {self.permission} >'
3640 3650
3641 3651
3642 3652 class UserGroupRepoToPerm(Base, BaseModel):
3643 3653 __tablename__ = 'users_group_repo_to_perm'
3644 3654 __table_args__ = (
3645 3655 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3646 3656 base_table_args
3647 3657 )
3648 3658
3649 3659 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3650 3660 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3651 3661 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3652 3662 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3653 3663
3654 3664 users_group = relationship('UserGroup', back_populates='users_group_repo_to_perm')
3655 3665 permission = relationship('Permission')
3656 3666 repository = relationship('Repository', back_populates='users_group_to_perm')
3657 3667 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all', back_populates='user_group_repo_to_perm')
3658 3668
3659 3669 @classmethod
3660 3670 def create(cls, users_group, repository, permission):
3661 3671 n = cls()
3662 3672 n.users_group = users_group
3663 3673 n.repository = repository
3664 3674 n.permission = permission
3665 3675 Session().add(n)
3666 3676 return n
3667 3677
3668 3678 def __repr__(self):
3669 3679 return f'<UserGroupRepoToPerm:{self.users_group} => {self.repository} >'
3670 3680
3671 3681
3672 3682 class UserGroupUserGroupToPerm(Base, BaseModel):
3673 3683 __tablename__ = 'user_group_user_group_to_perm'
3674 3684 __table_args__ = (
3675 3685 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3676 3686 CheckConstraint('target_user_group_id != user_group_id'),
3677 3687 base_table_args
3678 3688 )
3679 3689
3680 3690 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3681 3691 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3682 3692 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3683 3693 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3684 3694
3685 3695 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id', back_populates='user_group_user_group_to_perm')
3686 3696 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3687 3697 permission = relationship('Permission')
3688 3698
3689 3699 @classmethod
3690 3700 def create(cls, target_user_group, user_group, permission):
3691 3701 n = cls()
3692 3702 n.target_user_group = target_user_group
3693 3703 n.user_group = user_group
3694 3704 n.permission = permission
3695 3705 Session().add(n)
3696 3706 return n
3697 3707
3698 3708 def __repr__(self):
3699 3709 return f'<UserGroupUserGroup:{self.target_user_group} => {self.user_group} >'
3700 3710
3701 3711
3702 3712 class UserGroupToPerm(Base, BaseModel):
3703 3713 __tablename__ = 'users_group_to_perm'
3704 3714 __table_args__ = (
3705 3715 UniqueConstraint('users_group_id', 'permission_id',),
3706 3716 base_table_args
3707 3717 )
3708 3718
3709 3719 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3710 3720 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3711 3721 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3712 3722
3713 3723 users_group = relationship('UserGroup', back_populates='users_group_to_perm')
3714 3724 permission = relationship('Permission')
3715 3725
3716 3726
3717 3727 class UserRepoGroupToPerm(Base, BaseModel):
3718 3728 __tablename__ = 'user_repo_group_to_perm'
3719 3729 __table_args__ = (
3720 3730 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3721 3731 base_table_args
3722 3732 )
3723 3733
3724 3734 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3725 3735 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3726 3736 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3727 3737 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3728 3738
3729 3739 user = relationship('User', back_populates='repo_group_to_perm')
3730 3740 group = relationship('RepoGroup', back_populates='repo_group_to_perm')
3731 3741 permission = relationship('Permission')
3732 3742
3733 3743 @classmethod
3734 3744 def create(cls, user, repository_group, permission):
3735 3745 n = cls()
3736 3746 n.user = user
3737 3747 n.group = repository_group
3738 3748 n.permission = permission
3739 3749 Session().add(n)
3740 3750 return n
3741 3751
3742 3752
3743 3753 class UserGroupRepoGroupToPerm(Base, BaseModel):
3744 3754 __tablename__ = 'users_group_repo_group_to_perm'
3745 3755 __table_args__ = (
3746 3756 UniqueConstraint('users_group_id', 'group_id'),
3747 3757 base_table_args
3748 3758 )
3749 3759
3750 3760 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3751 3761 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3752 3762 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3753 3763 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3754 3764
3755 3765 users_group = relationship('UserGroup', back_populates='users_group_repo_group_to_perm')
3756 3766 permission = relationship('Permission')
3757 3767 group = relationship('RepoGroup', back_populates='users_group_to_perm')
3758 3768
3759 3769 @classmethod
3760 3770 def create(cls, user_group, repository_group, permission):
3761 3771 n = cls()
3762 3772 n.users_group = user_group
3763 3773 n.group = repository_group
3764 3774 n.permission = permission
3765 3775 Session().add(n)
3766 3776 return n
3767 3777
3768 3778 def __repr__(self):
3769 3779 return '<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3770 3780
3771 3781
3772 3782 class Statistics(Base, BaseModel):
3773 3783 __tablename__ = 'statistics'
3774 3784 __table_args__ = (
3775 3785 base_table_args
3776 3786 )
3777 3787
3778 3788 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3779 3789 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3780 3790 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3781 3791 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False) #JSON data
3782 3792 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False) #JSON data
3783 3793 languages = Column("languages", LargeBinary(1000000), nullable=False) #JSON data
3784 3794
3785 3795 repository = relationship('Repository', single_parent=True, viewonly=True)
3786 3796
3787 3797
3788 3798 class UserFollowing(Base, BaseModel):
3789 3799 __tablename__ = 'user_followings'
3790 3800 __table_args__ = (
3791 3801 UniqueConstraint('user_id', 'follows_repository_id'),
3792 3802 UniqueConstraint('user_id', 'follows_user_id'),
3793 3803 base_table_args
3794 3804 )
3795 3805
3796 3806 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3797 3807 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3798 3808 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3799 3809 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3800 3810 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3801 3811
3802 3812 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id', back_populates='followings')
3803 3813
3804 3814 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3805 3815 follows_repository = relationship('Repository', order_by='Repository.repo_name', back_populates='followers')
3806 3816
3807 3817 @classmethod
3808 3818 def get_repo_followers(cls, repo_id):
3809 3819 return cls.query().filter(cls.follows_repo_id == repo_id)
3810 3820
3811 3821
3812 3822 class CacheKey(Base, BaseModel):
3813 3823 __tablename__ = 'cache_invalidation'
3814 3824 __table_args__ = (
3815 3825 UniqueConstraint('cache_key'),
3816 3826 Index('key_idx', 'cache_key'),
3817 3827 Index('cache_args_idx', 'cache_args'),
3818 3828 base_table_args,
3819 3829 )
3820 3830
3821 3831 CACHE_TYPE_FEED = 'FEED'
3822 3832
3823 3833 # namespaces used to register process/thread aware caches
3824 3834 REPO_INVALIDATION_NAMESPACE = 'repo_cache.v1:{repo_id}'
3825 3835
3826 3836 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3827 3837 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3828 3838 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3829 3839 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3830 3840 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3831 3841
3832 3842 def __init__(self, cache_key, cache_args='', cache_state_uid=None, cache_active=False):
3833 3843 self.cache_key = cache_key
3834 3844 self.cache_args = cache_args
3835 3845 self.cache_active = cache_active
3836 3846 # first key should be same for all entries, since all workers should share it
3837 3847 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3838 3848
3839 3849 def __repr__(self):
3840 3850 return "<%s('%s:%s[%s]')>" % (
3841 3851 self.cls_name,
3842 3852 self.cache_id, self.cache_key, self.cache_active)
3843 3853
3844 3854 def _cache_key_partition(self):
3845 3855 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3846 3856 return prefix, repo_name, suffix
3847 3857
3848 3858 def get_prefix(self):
3849 3859 """
3850 3860 Try to extract prefix from existing cache key. The key could consist
3851 3861 of prefix, repo_name, suffix
3852 3862 """
3853 3863 # this returns prefix, repo_name, suffix
3854 3864 return self._cache_key_partition()[0]
3855 3865
3856 3866 def get_suffix(self):
3857 3867 """
3858 3868 get suffix that might have been used in _get_cache_key to
3859 3869 generate self.cache_key. Only used for informational purposes
3860 3870 in repo_edit.mako.
3861 3871 """
3862 3872 # prefix, repo_name, suffix
3863 3873 return self._cache_key_partition()[2]
3864 3874
3865 3875 @classmethod
3866 3876 def generate_new_state_uid(cls, based_on=None):
3867 3877 if based_on:
3868 3878 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3869 3879 else:
3870 3880 return str(uuid.uuid4())
3871 3881
3872 3882 @classmethod
3873 3883 def delete_all_cache(cls):
3874 3884 """
3875 3885 Delete all cache keys from database.
3876 3886 Should only be run when all instances are down and all entries
3877 3887 thus stale.
3878 3888 """
3879 3889 cls.query().delete()
3880 3890 Session().commit()
3881 3891
3882 3892 @classmethod
3883 3893 def set_invalidate(cls, cache_uid, delete=False):
3884 3894 """
3885 3895 Mark all caches of a repo as invalid in the database.
3886 3896 """
3887 3897 try:
3888 3898 qry = Session().query(cls).filter(cls.cache_key == cache_uid)
3889 3899 if delete:
3890 3900 qry.delete()
3891 3901 log.debug('cache objects deleted for cache args %s',
3892 3902 safe_str(cache_uid))
3893 3903 else:
3894 3904 new_uid = cls.generate_new_state_uid()
3895 3905 qry.update({"cache_state_uid": new_uid,
3896 3906 "cache_args": f"repo_state:{time.time()}"})
3897 3907 log.debug('cache object %s set new UID %s',
3898 3908 safe_str(cache_uid), new_uid)
3899 3909
3900 3910 Session().commit()
3901 3911 except Exception:
3902 3912 log.exception(
3903 3913 'Cache key invalidation failed for cache args %s',
3904 3914 safe_str(cache_uid))
3905 3915 Session().rollback()
3906 3916
3907 3917 @classmethod
3908 3918 def get_active_cache(cls, cache_key):
3909 3919 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3910 3920 if inv_obj:
3911 3921 return inv_obj
3912 3922 return None
3913 3923
3914 3924 @classmethod
3915 3925 def get_namespace_map(cls, namespace):
3916 3926 return {
3917 3927 x.cache_key: x
3918 3928 for x in cls.query().filter(cls.cache_args == namespace)}
3919 3929
3920 3930
3921 3931 class ChangesetComment(Base, BaseModel):
3922 3932 __tablename__ = 'changeset_comments'
3923 3933 __table_args__ = (
3924 3934 Index('cc_revision_idx', 'revision'),
3925 3935 base_table_args,
3926 3936 )
3927 3937
3928 3938 COMMENT_OUTDATED = 'comment_outdated'
3929 3939 COMMENT_TYPE_NOTE = 'note'
3930 3940 COMMENT_TYPE_TODO = 'todo'
3931 3941 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3932 3942
3933 3943 OP_IMMUTABLE = 'immutable'
3934 3944 OP_CHANGEABLE = 'changeable'
3935 3945
3936 3946 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3937 3947 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3938 3948 revision = Column('revision', String(40), nullable=True)
3939 3949 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3940 3950 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3941 3951 line_no = Column('line_no', Unicode(10), nullable=True)
3942 3952 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3943 3953 f_path = Column('f_path', Unicode(1000), nullable=True)
3944 3954 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3945 3955 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3946 3956 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3947 3957 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3948 3958 renderer = Column('renderer', Unicode(64), nullable=True)
3949 3959 display_state = Column('display_state', Unicode(128), nullable=True)
3950 3960 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3951 3961 draft = Column('draft', Boolean(), nullable=True, default=False)
3952 3962
3953 3963 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3954 3964 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3955 3965
3956 3966 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3957 3967 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3958 3968
3959 3969 author = relationship('User', lazy='select', back_populates='user_comments')
3960 3970 repo = relationship('Repository', back_populates='comments')
3961 3971 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='select', back_populates='comment')
3962 3972 pull_request = relationship('PullRequest', lazy='select', back_populates='comments')
3963 3973 pull_request_version = relationship('PullRequestVersion', lazy='select')
3964 3974 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='select', order_by='ChangesetCommentHistory.version', back_populates="comment")
3965 3975
3966 3976 @classmethod
3967 3977 def get_users(cls, revision=None, pull_request_id=None):
3968 3978 """
3969 3979 Returns user associated with this ChangesetComment. ie those
3970 3980 who actually commented
3971 3981
3972 3982 :param cls:
3973 3983 :param revision:
3974 3984 """
3975 3985 q = Session().query(User).join(ChangesetComment.author)
3976 3986 if revision:
3977 3987 q = q.filter(cls.revision == revision)
3978 3988 elif pull_request_id:
3979 3989 q = q.filter(cls.pull_request_id == pull_request_id)
3980 3990 return q.all()
3981 3991
3982 3992 @classmethod
3983 3993 def get_index_from_version(cls, pr_version, versions=None, num_versions=None) -> int:
3984 3994 if pr_version is None:
3985 3995 return 0
3986 3996
3987 3997 if versions is not None:
3988 3998 num_versions = [x.pull_request_version_id for x in versions]
3989 3999
3990 4000 num_versions = num_versions or []
3991 4001 try:
3992 4002 return num_versions.index(pr_version) + 1
3993 4003 except (IndexError, ValueError):
3994 4004 return 0
3995 4005
3996 4006 @property
3997 4007 def outdated(self):
3998 4008 return self.display_state == self.COMMENT_OUTDATED
3999 4009
4000 4010 @property
4001 4011 def outdated_js(self):
4002 4012 return str_json(self.display_state == self.COMMENT_OUTDATED)
4003 4013
4004 4014 @property
4005 4015 def immutable(self):
4006 4016 return self.immutable_state == self.OP_IMMUTABLE
4007 4017
4008 4018 def outdated_at_version(self, version: int) -> bool:
4009 4019 """
4010 4020 Checks if comment is outdated for given pull request version
4011 4021 """
4012 4022
4013 4023 def version_check():
4014 4024 return self.pull_request_version_id and self.pull_request_version_id != version
4015 4025
4016 4026 if self.is_inline:
4017 4027 return self.outdated and version_check()
4018 4028 else:
4019 4029 # general comments don't have .outdated set, also latest don't have a version
4020 4030 return version_check()
4021 4031
4022 4032 def outdated_at_version_js(self, version):
4023 4033 """
4024 4034 Checks if comment is outdated for given pull request version
4025 4035 """
4026 4036 return str_json(self.outdated_at_version(version))
4027 4037
4028 4038 def older_than_version(self, version: int) -> bool:
4029 4039 """
4030 4040 Checks if comment is made from a previous version than given.
4031 4041 Assumes self.pull_request_version.pull_request_version_id is an integer if not None.
4032 4042 """
4033 4043
4034 4044 # If version is None, return False as the current version cannot be less than None
4035 4045 if version is None:
4036 4046 return False
4037 4047
4038 4048 # Ensure that the version is an integer to prevent TypeError on comparison
4039 4049 if not isinstance(version, int):
4040 4050 raise ValueError("The provided version must be an integer.")
4041 4051
4042 4052 # Initialize current version to 0 or pull_request_version_id if it's available
4043 4053 cur_ver = 0
4044 4054 if self.pull_request_version and self.pull_request_version.pull_request_version_id is not None:
4045 4055 cur_ver = self.pull_request_version.pull_request_version_id
4046 4056
4047 4057 # Return True if the current version is less than the given version
4048 4058 return cur_ver < version
4049 4059
4050 4060 def older_than_version_js(self, version):
4051 4061 """
4052 4062 Checks if comment is made from previous version than given
4053 4063 """
4054 4064 return str_json(self.older_than_version(version))
4055 4065
4056 4066 @property
4057 4067 def commit_id(self):
4058 4068 """New style naming to stop using .revision"""
4059 4069 return self.revision
4060 4070
4061 4071 @property
4062 4072 def resolved(self):
4063 4073 return self.resolved_by[0] if self.resolved_by else None
4064 4074
4065 4075 @property
4066 4076 def is_todo(self):
4067 4077 return self.comment_type == self.COMMENT_TYPE_TODO
4068 4078
4069 4079 @property
4070 4080 def is_inline(self):
4071 4081 if self.line_no and self.f_path:
4072 4082 return True
4073 4083 return False
4074 4084
4075 4085 @property
4076 4086 def last_version(self):
4077 4087 version = 0
4078 4088 if self.history:
4079 4089 version = self.history[-1].version
4080 4090 return version
4081 4091
4082 4092 def get_index_version(self, versions):
4083 4093 return self.get_index_from_version(
4084 4094 self.pull_request_version_id, versions)
4085 4095
4086 4096 @property
4087 4097 def review_status(self):
4088 4098 if self.status_change:
4089 4099 return self.status_change[0].status
4090 4100
4091 4101 @property
4092 4102 def review_status_lbl(self):
4093 4103 if self.status_change:
4094 4104 return self.status_change[0].status_lbl
4095 4105
4096 4106 def __repr__(self):
4097 4107 if self.comment_id:
4098 4108 return f'<DB:Comment #{self.comment_id}>'
4099 4109 else:
4100 4110 return f'<DB:Comment at {id(self)!r}>'
4101 4111
4102 4112 def get_api_data(self):
4103 4113 comment = self
4104 4114
4105 4115 data = {
4106 4116 'comment_id': comment.comment_id,
4107 4117 'comment_type': comment.comment_type,
4108 4118 'comment_text': comment.text,
4109 4119 'comment_status': comment.status_change,
4110 4120 'comment_f_path': comment.f_path,
4111 4121 'comment_lineno': comment.line_no,
4112 4122 'comment_author': comment.author,
4113 4123 'comment_created_on': comment.created_on,
4114 4124 'comment_resolved_by': self.resolved,
4115 4125 'comment_commit_id': comment.revision,
4116 4126 'comment_pull_request_id': comment.pull_request_id,
4117 4127 'comment_last_version': self.last_version
4118 4128 }
4119 4129 return data
4120 4130
4121 4131 def __json__(self):
4122 4132 data = dict()
4123 4133 data.update(self.get_api_data())
4124 4134 return data
4125 4135
4126 4136
4127 4137 class ChangesetCommentHistory(Base, BaseModel):
4128 4138 __tablename__ = 'changeset_comments_history'
4129 4139 __table_args__ = (
4130 4140 Index('cch_comment_id_idx', 'comment_id'),
4131 4141 base_table_args,
4132 4142 )
4133 4143
4134 4144 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
4135 4145 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
4136 4146 version = Column("version", Integer(), nullable=False, default=0)
4137 4147 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
4138 4148 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
4139 4149 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4140 4150 deleted = Column('deleted', Boolean(), default=False)
4141 4151
4142 4152 author = relationship('User', lazy='joined')
4143 4153 comment = relationship('ChangesetComment', cascade="all, delete", back_populates="history")
4144 4154
4145 4155 @classmethod
4146 4156 def get_version(cls, comment_id):
4147 4157 q = Session().query(ChangesetCommentHistory).filter(
4148 4158 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
4149 4159 if q.count() == 0:
4150 4160 return 1
4151 4161 elif q.count() >= q[0].version:
4152 4162 return q.count() + 1
4153 4163 else:
4154 4164 return q[0].version + 1
4155 4165
4156 4166
4157 4167 class ChangesetStatus(Base, BaseModel):
4158 4168 __tablename__ = 'changeset_statuses'
4159 4169 __table_args__ = (
4160 4170 Index('cs_revision_idx', 'revision'),
4161 4171 Index('cs_version_idx', 'version'),
4162 4172 UniqueConstraint('repo_id', 'revision', 'version'),
4163 4173 base_table_args
4164 4174 )
4165 4175
4166 4176 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
4167 4177 STATUS_APPROVED = 'approved'
4168 4178 STATUS_REJECTED = 'rejected'
4169 4179 STATUS_UNDER_REVIEW = 'under_review'
4170 4180
4171 4181 STATUSES = [
4172 4182 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
4173 4183 (STATUS_APPROVED, _("Approved")),
4174 4184 (STATUS_REJECTED, _("Rejected")),
4175 4185 (STATUS_UNDER_REVIEW, _("Under Review")),
4176 4186 ]
4177 4187
4178 4188 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
4179 4189 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
4180 4190 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
4181 4191 revision = Column('revision', String(40), nullable=False)
4182 4192 status = Column('status', String(128), nullable=False, default=DEFAULT)
4183 4193 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
4184 4194 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
4185 4195 version = Column('version', Integer(), nullable=False, default=0)
4186 4196 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
4187 4197
4188 4198 author = relationship('User', lazy='select')
4189 4199 repo = relationship('Repository', lazy='select')
4190 4200 comment = relationship('ChangesetComment', lazy='select', back_populates='status_change')
4191 4201 pull_request = relationship('PullRequest', lazy='select', back_populates='statuses')
4192 4202
4193 4203 def __repr__(self):
4194 4204 return f"<{self.cls_name}('{self.status}[v{self.version}]:{self.author}')>"
4195 4205
4196 4206 @classmethod
4197 4207 def get_status_lbl(cls, value):
4198 4208 return dict(cls.STATUSES).get(value)
4199 4209
4200 4210 @property
4201 4211 def status_lbl(self):
4202 4212 return ChangesetStatus.get_status_lbl(self.status)
4203 4213
4204 4214 def get_api_data(self):
4205 4215 status = self
4206 4216 data = {
4207 4217 'status_id': status.changeset_status_id,
4208 4218 'status': status.status,
4209 4219 }
4210 4220 return data
4211 4221
4212 4222 def __json__(self):
4213 4223 data = dict()
4214 4224 data.update(self.get_api_data())
4215 4225 return data
4216 4226
4217 4227
4218 4228 class _SetState(object):
4219 4229 """
4220 4230 Context processor allowing changing state for sensitive operation such as
4221 4231 pull request update or merge
4222 4232 """
4223 4233
4224 4234 def __init__(self, pull_request, pr_state, back_state=None):
4225 4235 self._pr = pull_request
4226 4236 self._org_state = back_state or pull_request.pull_request_state
4227 4237 self._pr_state = pr_state
4228 4238 self._current_state = None
4229 4239
4230 4240 def __enter__(self):
4231 4241 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4232 4242 self._pr, self._pr_state)
4233 4243 self.set_pr_state(self._pr_state)
4234 4244 return self
4235 4245
4236 4246 def __exit__(self, exc_type, exc_val, exc_tb):
4237 4247 if exc_val is not None or exc_type is not None:
4238 4248 log.error(traceback.format_tb(exc_tb))
4239 4249 return None
4240 4250
4241 4251 self.set_pr_state(self._org_state)
4242 4252 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4243 4253 self._pr, self._org_state)
4244 4254
4245 4255 @property
4246 4256 def state(self):
4247 4257 return self._current_state
4248 4258
4249 4259 def set_pr_state(self, pr_state):
4250 4260 try:
4251 4261 self._pr.pull_request_state = pr_state
4252 4262 Session().add(self._pr)
4253 4263 Session().commit()
4254 4264 self._current_state = pr_state
4255 4265 except Exception:
4256 4266 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4257 4267 raise
4258 4268
4259 4269
4260 4270 class _PullRequestBase(BaseModel):
4261 4271 """
4262 4272 Common attributes of pull request and version entries.
4263 4273 """
4264 4274
4265 4275 # .status values
4266 4276 STATUS_NEW = 'new'
4267 4277 STATUS_OPEN = 'open'
4268 4278 STATUS_CLOSED = 'closed'
4269 4279
4270 4280 # available states
4271 4281 STATE_CREATING = 'creating'
4272 4282 STATE_UPDATING = 'updating'
4273 4283 STATE_MERGING = 'merging'
4274 4284 STATE_CREATED = 'created'
4275 4285
4276 4286 title = Column('title', Unicode(255), nullable=True)
4277 4287 description = Column(
4278 4288 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4279 4289 nullable=True)
4280 4290 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4281 4291
4282 4292 # new/open/closed status of pull request (not approve/reject/etc)
4283 4293 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4284 4294 created_on = Column(
4285 4295 'created_on', DateTime(timezone=False), nullable=False,
4286 4296 default=datetime.datetime.now)
4287 4297 updated_on = Column(
4288 4298 'updated_on', DateTime(timezone=False), nullable=False,
4289 4299 default=datetime.datetime.now)
4290 4300
4291 4301 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4292 4302
4293 4303 @declared_attr
4294 4304 def user_id(cls):
4295 4305 return Column(
4296 4306 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4297 4307 unique=None)
4298 4308
4299 4309 # 500 revisions max
4300 4310 _revisions = Column(
4301 4311 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4302 4312
4303 4313 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4304 4314
4305 4315 @declared_attr
4306 4316 def source_repo_id(cls):
4307 4317 # TODO: dan: rename column to source_repo_id
4308 4318 return Column(
4309 4319 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4310 4320 nullable=False)
4311 4321
4312 4322 @declared_attr
4313 4323 def pr_source(cls):
4314 4324 return relationship(
4315 4325 'Repository',
4316 4326 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4317 4327 overlaps="pull_requests_source"
4318 4328 )
4319 4329
4320 4330 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4321 4331
4322 4332 @hybrid_property
4323 4333 def source_ref(self):
4324 4334 return self._source_ref
4325 4335
4326 4336 @source_ref.setter
4327 4337 def source_ref(self, val):
4328 4338 parts = (val or '').split(':')
4329 4339 if len(parts) != 3:
4330 4340 raise ValueError(
4331 4341 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4332 4342 self._source_ref = safe_str(val)
4333 4343
4334 4344 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4335 4345
4336 4346 @hybrid_property
4337 4347 def target_ref(self):
4338 4348 return self._target_ref
4339 4349
4340 4350 @target_ref.setter
4341 4351 def target_ref(self, val):
4342 4352 parts = (val or '').split(':')
4343 4353 if len(parts) != 3:
4344 4354 raise ValueError(
4345 4355 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4346 4356 self._target_ref = safe_str(val)
4347 4357
4348 4358 @declared_attr
4349 4359 def target_repo_id(cls):
4350 4360 # TODO: dan: rename column to target_repo_id
4351 4361 return Column(
4352 4362 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4353 4363 nullable=False)
4354 4364
4355 4365 @declared_attr
4356 4366 def pr_target(cls):
4357 4367 return relationship(
4358 4368 'Repository',
4359 4369 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4360 4370 overlaps="pull_requests_target"
4361 4371 )
4362 4372
4363 4373 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4364 4374
4365 4375 # TODO: dan: rename column to last_merge_source_rev
4366 4376 _last_merge_source_rev = Column(
4367 4377 'last_merge_org_rev', String(40), nullable=True)
4368 4378 # TODO: dan: rename column to last_merge_target_rev
4369 4379 _last_merge_target_rev = Column(
4370 4380 'last_merge_other_rev', String(40), nullable=True)
4371 4381 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4372 4382 last_merge_metadata = Column(
4373 4383 'last_merge_metadata', MutationObj.as_mutable(
4374 4384 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4375 4385
4376 4386 merge_rev = Column('merge_rev', String(40), nullable=True)
4377 4387
4378 4388 reviewer_data = Column(
4379 4389 'reviewer_data_json', MutationObj.as_mutable(
4380 4390 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4381 4391
4382 4392 @property
4383 4393 def reviewer_data_json(self):
4384 4394 return str_json(self.reviewer_data)
4385 4395
4386 4396 @property
4387 4397 def last_merge_metadata_parsed(self):
4388 4398 metadata = {}
4389 4399 if not self.last_merge_metadata:
4390 4400 return metadata
4391 4401
4392 4402 if hasattr(self.last_merge_metadata, 'de_coerce'):
4393 4403 for k, v in self.last_merge_metadata.de_coerce().items():
4394 4404 if k in ['target_ref', 'source_ref']:
4395 4405 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4396 4406 else:
4397 4407 if hasattr(v, 'de_coerce'):
4398 4408 metadata[k] = v.de_coerce()
4399 4409 else:
4400 4410 metadata[k] = v
4401 4411 return metadata
4402 4412
4403 4413 @property
4404 4414 def work_in_progress(self):
4405 4415 """checks if pull request is work in progress by checking the title"""
4406 4416 title = self.title.upper()
4407 4417 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4408 4418 return True
4409 4419 return False
4410 4420
4411 4421 @property
4412 4422 def title_safe(self):
4413 4423 return self.title\
4414 4424 .replace('{', '{{')\
4415 4425 .replace('}', '}}')
4416 4426
4417 4427 @hybrid_property
4418 4428 def description_safe(self):
4419 4429 return description_escaper(self.description)
4420 4430
4421 4431 @hybrid_property
4422 4432 def revisions(self):
4423 4433 return self._revisions.split(':') if self._revisions else []
4424 4434
4425 4435 @revisions.setter
4426 4436 def revisions(self, val):
4427 4437 self._revisions = ':'.join(val)
4428 4438
4429 4439 @hybrid_property
4430 4440 def last_merge_status(self):
4431 4441 return safe_int(self._last_merge_status)
4432 4442
4433 4443 @last_merge_status.setter
4434 4444 def last_merge_status(self, val):
4435 4445 self._last_merge_status = val
4436 4446
4437 4447 @declared_attr
4438 4448 def author(cls):
4439 4449 return relationship(
4440 4450 'User', lazy='joined',
4441 4451 #TODO, problem that is somehow :?
4442 4452 #back_populates='user_pull_requests'
4443 4453 )
4444 4454
4445 4455 @declared_attr
4446 4456 def source_repo(cls):
4447 4457 return relationship(
4448 4458 'Repository',
4449 4459 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4450 4460 overlaps="pr_source"
4451 4461 )
4452 4462
4453 4463 @property
4454 4464 def source_ref_parts(self):
4455 4465 return self.unicode_to_reference(self.source_ref)
4456 4466
4457 4467 @declared_attr
4458 4468 def target_repo(cls):
4459 4469 return relationship(
4460 4470 'Repository',
4461 4471 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4462 4472 overlaps="pr_target"
4463 4473 )
4464 4474
4465 4475 @property
4466 4476 def target_ref_parts(self):
4467 4477 return self.unicode_to_reference(self.target_ref)
4468 4478
4469 4479 @property
4470 4480 def shadow_merge_ref(self):
4471 4481 return self.unicode_to_reference(self._shadow_merge_ref)
4472 4482
4473 4483 @shadow_merge_ref.setter
4474 4484 def shadow_merge_ref(self, ref):
4475 4485 self._shadow_merge_ref = self.reference_to_unicode(ref)
4476 4486
4477 4487 @staticmethod
4478 4488 def unicode_to_reference(raw):
4479 4489 return unicode_to_reference(raw)
4480 4490
4481 4491 @staticmethod
4482 4492 def reference_to_unicode(ref):
4483 4493 return reference_to_unicode(ref)
4484 4494
4485 4495 def get_api_data(self, with_merge_state=True):
4486 4496 from rhodecode.model.pull_request import PullRequestModel
4487 4497
4488 4498 pull_request = self
4489 4499 if with_merge_state:
4490 4500 merge_response, merge_status, msg = \
4491 4501 PullRequestModel().merge_status(pull_request)
4492 4502 merge_state = {
4493 4503 'status': merge_status,
4494 4504 'message': safe_str(msg),
4495 4505 }
4496 4506 else:
4497 4507 merge_state = {'status': 'not_available',
4498 4508 'message': 'not_available'}
4499 4509
4500 4510 merge_data = {
4501 4511 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4502 4512 'reference': (
4503 4513 pull_request.shadow_merge_ref.asdict()
4504 4514 if pull_request.shadow_merge_ref else None),
4505 4515 }
4506 4516
4507 4517 data = {
4508 4518 'pull_request_id': pull_request.pull_request_id,
4509 4519 'url': PullRequestModel().get_url(pull_request),
4510 4520 'title': pull_request.title,
4511 4521 'description': pull_request.description,
4512 4522 'status': pull_request.status,
4513 4523 'state': pull_request.pull_request_state,
4514 4524 'created_on': pull_request.created_on,
4515 4525 'updated_on': pull_request.updated_on,
4516 4526 'commit_ids': pull_request.revisions,
4517 4527 'review_status': pull_request.calculated_review_status(),
4518 4528 'mergeable': merge_state,
4519 4529 'source': {
4520 4530 'clone_url': pull_request.source_repo.clone_url(),
4521 4531 'repository': pull_request.source_repo.repo_name,
4522 4532 'reference': {
4523 4533 'name': pull_request.source_ref_parts.name,
4524 4534 'type': pull_request.source_ref_parts.type,
4525 4535 'commit_id': pull_request.source_ref_parts.commit_id,
4526 4536 },
4527 4537 },
4528 4538 'target': {
4529 4539 'clone_url': pull_request.target_repo.clone_url(),
4530 4540 'repository': pull_request.target_repo.repo_name,
4531 4541 'reference': {
4532 4542 'name': pull_request.target_ref_parts.name,
4533 4543 'type': pull_request.target_ref_parts.type,
4534 4544 'commit_id': pull_request.target_ref_parts.commit_id,
4535 4545 },
4536 4546 },
4537 4547 'merge': merge_data,
4538 4548 'author': pull_request.author.get_api_data(include_secrets=False,
4539 4549 details='basic'),
4540 4550 'reviewers': [
4541 4551 {
4542 4552 'user': reviewer.get_api_data(include_secrets=False,
4543 4553 details='basic'),
4544 4554 'reasons': reasons,
4545 4555 'review_status': st[0][1].status if st else 'not_reviewed',
4546 4556 }
4547 4557 for obj, reviewer, reasons, mandatory, st in
4548 4558 pull_request.reviewers_statuses()
4549 4559 ]
4550 4560 }
4551 4561
4552 4562 return data
4553 4563
4554 4564 def set_state(self, pull_request_state, final_state=None):
4555 4565 """
4556 4566 # goes from initial state to updating to initial state.
4557 4567 # initial state can be changed by specifying back_state=
4558 4568 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4559 4569 pull_request.merge()
4560 4570
4561 4571 :param pull_request_state:
4562 4572 :param final_state:
4563 4573
4564 4574 """
4565 4575
4566 4576 return _SetState(self, pull_request_state, back_state=final_state)
4567 4577
4568 4578
4569 4579 class PullRequest(Base, _PullRequestBase):
4570 4580 __tablename__ = 'pull_requests'
4571 4581 __table_args__ = (
4572 4582 base_table_args,
4573 4583 )
4574 4584 LATEST_VER = 'latest'
4575 4585
4576 4586 pull_request_id = Column(
4577 4587 'pull_request_id', Integer(), nullable=False, primary_key=True)
4578 4588
4579 4589 def __repr__(self):
4580 4590 if self.pull_request_id:
4581 4591 return f'<DB:PullRequest #{self.pull_request_id}>'
4582 4592 else:
4583 4593 return f'<DB:PullRequest at {id(self)!r}>'
4584 4594
4585 4595 def __str__(self):
4586 4596 if self.pull_request_id:
4587 4597 return f'#{self.pull_request_id}'
4588 4598 else:
4589 4599 return f'#{id(self)!r}'
4590 4600
4591 4601 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan", back_populates='pull_request')
4592 4602 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan", back_populates='pull_request')
4593 4603 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='pull_request')
4594 4604 versions = relationship('PullRequestVersion', cascade="all, delete-orphan", lazy='dynamic', back_populates='pull_request')
4595 4605
4596 4606 @classmethod
4597 4607 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4598 4608 internal_methods=None):
4599 4609
4600 4610 class PullRequestDisplay(object):
4601 4611 """
4602 4612 Special object wrapper for showing PullRequest data via Versions
4603 4613 It mimics PR object as close as possible. This is read only object
4604 4614 just for display
4605 4615 """
4606 4616
4607 4617 def __init__(self, attrs, internal=None):
4608 4618 self.attrs = attrs
4609 4619 # internal have priority over the given ones via attrs
4610 4620 self.internal = internal or ['versions']
4611 4621
4612 4622 def __getattr__(self, item):
4613 4623 if item in self.internal:
4614 4624 return getattr(self, item)
4615 4625 try:
4616 4626 return self.attrs[item]
4617 4627 except KeyError:
4618 4628 raise AttributeError(
4619 4629 '%s object has no attribute %s' % (self, item))
4620 4630
4621 4631 def __repr__(self):
4622 4632 pr_id = self.attrs.get('pull_request_id')
4623 4633 return f'<DB:PullRequestDisplay #{pr_id}>'
4624 4634
4625 4635 def versions(self):
4626 4636 return pull_request_obj.versions.order_by(
4627 4637 PullRequestVersion.pull_request_version_id).all()
4628 4638
4629 4639 def is_closed(self):
4630 4640 return pull_request_obj.is_closed()
4631 4641
4632 4642 def is_state_changing(self):
4633 4643 return pull_request_obj.is_state_changing()
4634 4644
4635 4645 @property
4636 4646 def pull_request_version_id(self):
4637 4647 return getattr(pull_request_obj, 'pull_request_version_id', None)
4638 4648
4639 4649 @property
4640 4650 def pull_request_last_version(self):
4641 4651 return pull_request_obj.pull_request_last_version
4642 4652
4643 4653 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4644 4654
4645 4655 attrs.author = StrictAttributeDict(
4646 4656 pull_request_obj.author.get_api_data())
4647 4657 if pull_request_obj.target_repo:
4648 4658 attrs.target_repo = StrictAttributeDict(
4649 4659 pull_request_obj.target_repo.get_api_data())
4650 4660 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4651 4661
4652 4662 if pull_request_obj.source_repo:
4653 4663 attrs.source_repo = StrictAttributeDict(
4654 4664 pull_request_obj.source_repo.get_api_data())
4655 4665 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4656 4666
4657 4667 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4658 4668 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4659 4669 attrs.revisions = pull_request_obj.revisions
4660 4670 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4661 4671 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4662 4672 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4663 4673 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4664 4674
4665 4675 return PullRequestDisplay(attrs, internal=internal_methods)
4666 4676
4667 4677 def is_closed(self):
4668 4678 return self.status == self.STATUS_CLOSED
4669 4679
4670 4680 def is_state_changing(self):
4671 4681 return self.pull_request_state != PullRequest.STATE_CREATED
4672 4682
4673 4683 def __json__(self):
4674 4684 return {
4675 4685 'revisions': self.revisions,
4676 4686 'versions': self.versions_count
4677 4687 }
4678 4688
4679 4689 def calculated_review_status(self):
4680 4690 from rhodecode.model.changeset_status import ChangesetStatusModel
4681 4691 return ChangesetStatusModel().calculated_review_status(self)
4682 4692
4683 4693 def reviewers_statuses(self, user=None):
4684 4694 from rhodecode.model.changeset_status import ChangesetStatusModel
4685 4695 return ChangesetStatusModel().reviewers_statuses(self, user=user)
4686 4696
4687 4697 def get_pull_request_reviewers(self, role=None):
4688 4698 qry = PullRequestReviewers.query()\
4689 4699 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4690 4700 if role:
4691 4701 qry = qry.filter(PullRequestReviewers.role == role)
4692 4702
4693 4703 return qry.all()
4694 4704
4695 4705 @property
4696 4706 def reviewers_count(self):
4697 4707 qry = PullRequestReviewers.query()\
4698 4708 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4699 4709 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4700 4710 return qry.count()
4701 4711
4702 4712 @property
4703 4713 def observers_count(self):
4704 4714 qry = PullRequestReviewers.query()\
4705 4715 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4706 4716 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4707 4717 return qry.count()
4708 4718
4709 4719 def observers(self):
4710 4720 qry = PullRequestReviewers.query()\
4711 4721 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4712 4722 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4713 4723 .all()
4714 4724
4715 4725 for entry in qry:
4716 4726 yield entry, entry.user
4717 4727
4718 4728 @property
4719 4729 def workspace_id(self):
4720 4730 from rhodecode.model.pull_request import PullRequestModel
4721 4731 return PullRequestModel()._workspace_id(self)
4722 4732
4723 4733 def get_shadow_repo(self):
4724 4734 workspace_id = self.workspace_id
4725 4735 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4726 4736 if os.path.isdir(shadow_repository_path):
4727 4737 vcs_obj = self.target_repo.scm_instance()
4728 4738 return vcs_obj.get_shadow_instance(shadow_repository_path)
4729 4739
4730 4740 @property
4731 4741 def versions_count(self):
4732 4742 """
4733 4743 return number of versions this PR have, e.g a PR that once been
4734 4744 updated will have 2 versions
4735 4745 """
4736 4746 return self.versions.count() + 1
4737 4747
4738 4748 @property
4739 4749 def pull_request_last_version(self):
4740 4750 return self.versions_count
4741 4751
4742 4752
4743 4753 class PullRequestVersion(Base, _PullRequestBase):
4744 4754 __tablename__ = 'pull_request_versions'
4745 4755 __table_args__ = (
4746 4756 base_table_args,
4747 4757 )
4748 4758
4749 4759 pull_request_version_id = Column('pull_request_version_id', Integer(), nullable=False, primary_key=True)
4750 4760 pull_request_id = Column('pull_request_id', Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
4751 4761 pull_request = relationship('PullRequest', back_populates='versions')
4752 4762
4753 4763 def __repr__(self):
4754 4764 if self.pull_request_version_id:
4755 4765 return f'<DB:PullRequestVersion #{self.pull_request_version_id}>'
4756 4766 else:
4757 4767 return f'<DB:PullRequestVersion at {id(self)!r}>'
4758 4768
4759 4769 @property
4760 4770 def reviewers(self):
4761 4771 return self.pull_request.reviewers
4762 4772
4763 4773 @property
4764 4774 def versions(self):
4765 4775 return self.pull_request.versions
4766 4776
4767 4777 def is_closed(self):
4768 4778 # calculate from original
4769 4779 return self.pull_request.status == self.STATUS_CLOSED
4770 4780
4771 4781 def is_state_changing(self):
4772 4782 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4773 4783
4774 4784 def calculated_review_status(self):
4775 4785 return self.pull_request.calculated_review_status()
4776 4786
4777 4787 def reviewers_statuses(self):
4778 4788 return self.pull_request.reviewers_statuses()
4779 4789
4780 4790 def observers(self):
4781 4791 return self.pull_request.observers()
4782 4792
4783 4793
4784 4794 class PullRequestReviewers(Base, BaseModel):
4785 4795 __tablename__ = 'pull_request_reviewers'
4786 4796 __table_args__ = (
4787 4797 base_table_args,
4788 4798 )
4789 4799 ROLE_REVIEWER = 'reviewer'
4790 4800 ROLE_OBSERVER = 'observer'
4791 4801 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4792 4802
4793 4803 @hybrid_property
4794 4804 def reasons(self):
4795 4805 if not self._reasons:
4796 4806 return []
4797 4807 return self._reasons
4798 4808
4799 4809 @reasons.setter
4800 4810 def reasons(self, val):
4801 4811 val = val or []
4802 4812 if any(not isinstance(x, str) for x in val):
4803 4813 raise Exception('invalid reasons type, must be list of strings')
4804 4814 self._reasons = val
4805 4815
4806 4816 pull_requests_reviewers_id = Column(
4807 4817 'pull_requests_reviewers_id', Integer(), nullable=False,
4808 4818 primary_key=True)
4809 4819 pull_request_id = Column(
4810 4820 "pull_request_id", Integer(),
4811 4821 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4812 4822 user_id = Column(
4813 4823 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4814 4824 _reasons = Column(
4815 4825 'reason', MutationList.as_mutable(
4816 4826 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4817 4827
4818 4828 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4819 4829 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4820 4830
4821 4831 user = relationship('User')
4822 4832 pull_request = relationship('PullRequest', back_populates='reviewers')
4823 4833
4824 4834 rule_data = Column(
4825 4835 'rule_data_json',
4826 4836 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4827 4837
4828 4838 def rule_user_group_data(self):
4829 4839 """
4830 4840 Returns the voting user group rule data for this reviewer
4831 4841 """
4832 4842
4833 4843 if self.rule_data and 'vote_rule' in self.rule_data:
4834 4844 user_group_data = {}
4835 4845 if 'rule_user_group_entry_id' in self.rule_data:
4836 4846 # means a group with voting rules !
4837 4847 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4838 4848 user_group_data['name'] = self.rule_data['rule_name']
4839 4849 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4840 4850
4841 4851 return user_group_data
4842 4852
4843 4853 @classmethod
4844 4854 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4845 4855 qry = PullRequestReviewers.query()\
4846 4856 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4847 4857 if role:
4848 4858 qry = qry.filter(PullRequestReviewers.role == role)
4849 4859
4850 4860 return qry.all()
4851 4861
4852 4862 def __repr__(self):
4853 4863 return f"<{self.cls_name}('id:{self.pull_requests_reviewers_id}')>"
4854 4864
4855 4865
4856 4866 class Notification(Base, BaseModel):
4857 4867 __tablename__ = 'notifications'
4858 4868 __table_args__ = (
4859 4869 Index('notification_type_idx', 'type'),
4860 4870 base_table_args,
4861 4871 )
4862 4872
4863 4873 TYPE_CHANGESET_COMMENT = 'cs_comment'
4864 4874 TYPE_MESSAGE = 'message'
4865 4875 TYPE_MENTION = 'mention'
4866 4876 TYPE_REGISTRATION = 'registration'
4867 4877 TYPE_PULL_REQUEST = 'pull_request'
4868 4878 TYPE_PULL_REQUEST_COMMENT = 'pull_request_comment'
4869 4879 TYPE_PULL_REQUEST_UPDATE = 'pull_request_update'
4870 4880
4871 4881 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4872 4882 subject = Column('subject', Unicode(512), nullable=True)
4873 4883 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4874 4884 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4875 4885 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4876 4886 type_ = Column('type', Unicode(255))
4877 4887
4878 4888 created_by_user = relationship('User', back_populates='user_created_notifications')
4879 4889 notifications_to_users = relationship('UserNotification', lazy='joined', cascade="all, delete-orphan", back_populates='notification')
4880 4890
4881 4891 @property
4882 4892 def recipients(self):
4883 4893 return [x.user for x in UserNotification.query()\
4884 4894 .filter(UserNotification.notification == self)\
4885 4895 .order_by(UserNotification.user_id.asc()).all()]
4886 4896
4887 4897 @classmethod
4888 4898 def create(cls, created_by, subject, body, recipients, type_=None):
4889 4899 if type_ is None:
4890 4900 type_ = Notification.TYPE_MESSAGE
4891 4901
4892 4902 notification = cls()
4893 4903 notification.created_by_user = created_by
4894 4904 notification.subject = subject
4895 4905 notification.body = body
4896 4906 notification.type_ = type_
4897 4907 notification.created_on = datetime.datetime.now()
4898 4908
4899 4909 # For each recipient link the created notification to his account
4900 4910 for u in recipients:
4901 4911 assoc = UserNotification()
4902 4912 assoc.user_id = u.user_id
4903 4913 assoc.notification = notification
4904 4914
4905 4915 # if created_by is inside recipients mark his notification
4906 4916 # as read
4907 4917 if u.user_id == created_by.user_id:
4908 4918 assoc.read = True
4909 4919 Session().add(assoc)
4910 4920
4911 4921 Session().add(notification)
4912 4922
4913 4923 return notification
4914 4924
4915 4925
4916 4926 class UserNotification(Base, BaseModel):
4917 4927 __tablename__ = 'user_to_notification'
4918 4928 __table_args__ = (
4919 4929 UniqueConstraint('user_id', 'notification_id'),
4920 4930 base_table_args
4921 4931 )
4922 4932
4923 4933 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4924 4934 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4925 4935 read = Column('read', Boolean, default=False)
4926 4936 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4927 4937
4928 4938 user = relationship('User', lazy="joined", back_populates='notifications')
4929 4939 notification = relationship('Notification', lazy="joined", order_by=lambda: Notification.created_on.desc(), back_populates='notifications_to_users')
4930 4940
4931 4941 def mark_as_read(self):
4932 4942 self.read = True
4933 4943 Session().add(self)
4934 4944
4935 4945
4936 4946 class UserNotice(Base, BaseModel):
4937 4947 __tablename__ = 'user_notices'
4938 4948 __table_args__ = (
4939 4949 base_table_args
4940 4950 )
4941 4951
4942 4952 NOTIFICATION_TYPE_MESSAGE = 'message'
4943 4953 NOTIFICATION_TYPE_NOTICE = 'notice'
4944 4954
4945 4955 NOTIFICATION_LEVEL_INFO = 'info'
4946 4956 NOTIFICATION_LEVEL_WARNING = 'warning'
4947 4957 NOTIFICATION_LEVEL_ERROR = 'error'
4948 4958
4949 4959 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4950 4960
4951 4961 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4952 4962 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4953 4963
4954 4964 notice_read = Column('notice_read', Boolean, default=False)
4955 4965
4956 4966 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4957 4967 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4958 4968
4959 4969 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4960 4970 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4961 4971
4962 4972 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4963 4973 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4964 4974
4965 4975 @classmethod
4966 4976 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4967 4977
4968 4978 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4969 4979 cls.NOTIFICATION_LEVEL_WARNING,
4970 4980 cls.NOTIFICATION_LEVEL_INFO]:
4971 4981 return
4972 4982
4973 4983 from rhodecode.model.user import UserModel
4974 4984 user = UserModel().get_user(user)
4975 4985
4976 4986 new_notice = UserNotice()
4977 4987 if not allow_duplicate:
4978 4988 existing_msg = UserNotice().query() \
4979 4989 .filter(UserNotice.user == user) \
4980 4990 .filter(UserNotice.notice_body == body) \
4981 4991 .filter(UserNotice.notice_read == false()) \
4982 4992 .scalar()
4983 4993 if existing_msg:
4984 4994 log.warning('Ignoring duplicate notice for user %s', user)
4985 4995 return
4986 4996
4987 4997 new_notice.user = user
4988 4998 new_notice.notice_subject = subject
4989 4999 new_notice.notice_body = body
4990 5000 new_notice.notification_level = notice_level
4991 5001 Session().add(new_notice)
4992 5002 Session().commit()
4993 5003
4994 5004
4995 5005 class Gist(Base, BaseModel):
4996 5006 __tablename__ = 'gists'
4997 5007 __table_args__ = (
4998 5008 Index('g_gist_access_id_idx', 'gist_access_id'),
4999 5009 Index('g_created_on_idx', 'created_on'),
5000 5010 base_table_args
5001 5011 )
5002 5012
5003 5013 GIST_PUBLIC = 'public'
5004 5014 GIST_PRIVATE = 'private'
5005 5015 DEFAULT_FILENAME = 'gistfile1.txt'
5006 5016
5007 5017 ACL_LEVEL_PUBLIC = 'acl_public'
5008 5018 ACL_LEVEL_PRIVATE = 'acl_private'
5009 5019
5010 5020 gist_id = Column('gist_id', Integer(), primary_key=True)
5011 5021 gist_access_id = Column('gist_access_id', Unicode(250))
5012 5022 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
5013 5023 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
5014 5024 gist_expires = Column('gist_expires', Float(53), nullable=False)
5015 5025 gist_type = Column('gist_type', Unicode(128), nullable=False)
5016 5026 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5017 5027 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5018 5028 acl_level = Column('acl_level', Unicode(128), nullable=True)
5019 5029
5020 5030 owner = relationship('User', back_populates='user_gists')
5021 5031
5022 5032 def __repr__(self):
5023 5033 return f'<Gist:[{self.gist_type}]{self.gist_access_id}>'
5024 5034
5025 5035 @hybrid_property
5026 5036 def description_safe(self):
5027 5037 return description_escaper(self.gist_description)
5028 5038
5029 5039 @classmethod
5030 5040 def get_or_404(cls, id_):
5031 5041 from pyramid.httpexceptions import HTTPNotFound
5032 5042
5033 5043 res = cls.query().filter(cls.gist_access_id == id_).scalar()
5034 5044 if not res:
5035 5045 log.debug('WARN: No DB entry with id %s', id_)
5036 5046 raise HTTPNotFound()
5037 5047 return res
5038 5048
5039 5049 @classmethod
5040 5050 def get_by_access_id(cls, gist_access_id):
5041 5051 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
5042 5052
5043 5053 def gist_url(self):
5044 5054 from rhodecode.model.gist import GistModel
5045 5055 return GistModel().get_url(self)
5046 5056
5047 5057 @classmethod
5048 5058 def base_path(cls):
5049 5059 """
5050 5060 Returns base path when all gists are stored
5051 5061
5052 5062 :param cls:
5053 5063 """
5054 5064 from rhodecode.model.gist import GIST_STORE_LOC
5055 5065 from rhodecode.lib.utils import get_rhodecode_repo_store_path
5056 5066 repo_store_path = get_rhodecode_repo_store_path()
5057 5067 return os.path.join(repo_store_path, GIST_STORE_LOC)
5058 5068
5059 5069 def get_api_data(self):
5060 5070 """
5061 5071 Common function for generating gist related data for API
5062 5072 """
5063 5073 gist = self
5064 5074 data = {
5065 5075 'gist_id': gist.gist_id,
5066 5076 'type': gist.gist_type,
5067 5077 'access_id': gist.gist_access_id,
5068 5078 'description': gist.gist_description,
5069 5079 'url': gist.gist_url(),
5070 5080 'expires': gist.gist_expires,
5071 5081 'created_on': gist.created_on,
5072 5082 'modified_at': gist.modified_at,
5073 5083 'content': None,
5074 5084 'acl_level': gist.acl_level,
5075 5085 }
5076 5086 return data
5077 5087
5078 5088 def __json__(self):
5079 5089 data = dict()
5080 5090 data.update(self.get_api_data())
5081 5091 return data
5082 5092 # SCM functions
5083 5093
5084 5094 def scm_instance(self, **kwargs):
5085 5095 """
5086 5096 Get an instance of VCS Repository
5087 5097
5088 5098 :param kwargs:
5089 5099 """
5090 5100 from rhodecode.model.gist import GistModel
5091 5101 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
5092 5102 return get_vcs_instance(
5093 5103 repo_path=safe_str(full_repo_path), create=False,
5094 5104 _vcs_alias=GistModel.vcs_backend)
5095 5105
5096 5106
5097 5107 class ExternalIdentity(Base, BaseModel):
5098 5108 __tablename__ = 'external_identities'
5099 5109 __table_args__ = (
5100 5110 Index('local_user_id_idx', 'local_user_id'),
5101 5111 Index('external_id_idx', 'external_id'),
5102 5112 base_table_args
5103 5113 )
5104 5114
5105 5115 external_id = Column('external_id', Unicode(255), default='', primary_key=True)
5106 5116 external_username = Column('external_username', Unicode(1024), default='')
5107 5117 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
5108 5118 provider_name = Column('provider_name', Unicode(255), default='', primary_key=True)
5109 5119 access_token = Column('access_token', String(1024), default='')
5110 5120 alt_token = Column('alt_token', String(1024), default='')
5111 5121 token_secret = Column('token_secret', String(1024), default='')
5112 5122
5113 5123 @classmethod
5114 5124 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
5115 5125 """
5116 5126 Returns ExternalIdentity instance based on search params
5117 5127
5118 5128 :param external_id:
5119 5129 :param provider_name:
5120 5130 :return: ExternalIdentity
5121 5131 """
5122 5132 query = cls.query()
5123 5133 query = query.filter(cls.external_id == external_id)
5124 5134 query = query.filter(cls.provider_name == provider_name)
5125 5135 if local_user_id:
5126 5136 query = query.filter(cls.local_user_id == local_user_id)
5127 5137 return query.first()
5128 5138
5129 5139 @classmethod
5130 5140 def user_by_external_id_and_provider(cls, external_id, provider_name):
5131 5141 """
5132 5142 Returns User instance based on search params
5133 5143
5134 5144 :param external_id:
5135 5145 :param provider_name:
5136 5146 :return: User
5137 5147 """
5138 5148 query = User.query()
5139 5149 query = query.filter(cls.external_id == external_id)
5140 5150 query = query.filter(cls.provider_name == provider_name)
5141 5151 query = query.filter(User.user_id == cls.local_user_id)
5142 5152 return query.first()
5143 5153
5144 5154 @classmethod
5145 5155 def by_local_user_id(cls, local_user_id):
5146 5156 """
5147 5157 Returns all tokens for user
5148 5158
5149 5159 :param local_user_id:
5150 5160 :return: ExternalIdentity
5151 5161 """
5152 5162 query = cls.query()
5153 5163 query = query.filter(cls.local_user_id == local_user_id)
5154 5164 return query
5155 5165
5156 5166 @classmethod
5157 5167 def load_provider_plugin(cls, plugin_id):
5158 5168 from rhodecode.authentication.base import loadplugin
5159 5169 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
5160 5170 auth_plugin = loadplugin(_plugin_id)
5161 5171 return auth_plugin
5162 5172
5163 5173
5164 5174 class Integration(Base, BaseModel):
5165 5175 __tablename__ = 'integrations'
5166 5176 __table_args__ = (
5167 5177 base_table_args
5168 5178 )
5169 5179
5170 5180 integration_id = Column('integration_id', Integer(), primary_key=True)
5171 5181 integration_type = Column('integration_type', String(255))
5172 5182 enabled = Column('enabled', Boolean(), nullable=False)
5173 5183 name = Column('name', String(255), nullable=False)
5174 5184 child_repos_only = Column('child_repos_only', Boolean(), nullable=False, default=False)
5175 5185
5176 5186 settings = Column(
5177 5187 'settings_json', MutationObj.as_mutable(
5178 5188 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
5179 5189 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
5180 5190 repo = relationship('Repository', lazy='joined', back_populates='integrations')
5181 5191
5182 5192 repo_group_id = Column('repo_group_id', Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
5183 5193 repo_group = relationship('RepoGroup', lazy='joined', back_populates='integrations')
5184 5194
5185 5195 @property
5186 5196 def scope(self):
5187 5197 if self.repo:
5188 5198 return repr(self.repo)
5189 5199 if self.repo_group:
5190 5200 if self.child_repos_only:
5191 5201 return repr(self.repo_group) + ' (child repos only)'
5192 5202 else:
5193 5203 return repr(self.repo_group) + ' (recursive)'
5194 5204 if self.child_repos_only:
5195 5205 return 'root_repos'
5196 5206 return 'global'
5197 5207
5198 5208 def __repr__(self):
5199 5209 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
5200 5210
5201 5211
5202 5212 class RepoReviewRuleUser(Base, BaseModel):
5203 5213 __tablename__ = 'repo_review_rules_users'
5204 5214 __table_args__ = (
5205 5215 base_table_args
5206 5216 )
5207 5217 ROLE_REVIEWER = 'reviewer'
5208 5218 ROLE_OBSERVER = 'observer'
5209 5219 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5210 5220
5211 5221 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
5212 5222 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5213 5223 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
5214 5224 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5215 5225 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5216 5226 user = relationship('User', back_populates='user_review_rules')
5217 5227
5218 5228 def rule_data(self):
5219 5229 return {
5220 5230 'mandatory': self.mandatory,
5221 5231 'role': self.role,
5222 5232 }
5223 5233
5224 5234
5225 5235 class RepoReviewRuleUserGroup(Base, BaseModel):
5226 5236 __tablename__ = 'repo_review_rules_users_groups'
5227 5237 __table_args__ = (
5228 5238 base_table_args
5229 5239 )
5230 5240
5231 5241 VOTE_RULE_ALL = -1
5232 5242 ROLE_REVIEWER = 'reviewer'
5233 5243 ROLE_OBSERVER = 'observer'
5234 5244 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5235 5245
5236 5246 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
5237 5247 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5238 5248 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
5239 5249 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5240 5250 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5241 5251 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5242 5252 users_group = relationship('UserGroup')
5243 5253
5244 5254 def rule_data(self):
5245 5255 return {
5246 5256 'mandatory': self.mandatory,
5247 5257 'role': self.role,
5248 5258 'vote_rule': self.vote_rule
5249 5259 }
5250 5260
5251 5261 @property
5252 5262 def vote_rule_label(self):
5253 5263 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
5254 5264 return 'all must vote'
5255 5265 else:
5256 5266 return 'min. vote {}'.format(self.vote_rule)
5257 5267
5258 5268
5259 5269 class RepoReviewRule(Base, BaseModel):
5260 5270 __tablename__ = 'repo_review_rules'
5261 5271 __table_args__ = (
5262 5272 base_table_args
5263 5273 )
5264 5274
5265 5275 repo_review_rule_id = Column(
5266 5276 'repo_review_rule_id', Integer(), primary_key=True)
5267 5277 repo_id = Column(
5268 5278 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
5269 5279 repo = relationship('Repository', back_populates='review_rules')
5270 5280
5271 5281 review_rule_name = Column('review_rule_name', String(255))
5272 5282 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5273 5283 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5274 5284 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5275 5285
5276 5286 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5277 5287
5278 5288 # Legacy fields, just for backward compat
5279 5289 _forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5280 5290 _forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5281 5291
5282 5292 pr_author = Column("pr_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5283 5293 commit_author = Column("commit_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5284 5294
5285 5295 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5286 5296
5287 5297 rule_users = relationship('RepoReviewRuleUser')
5288 5298 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5289 5299
5290 5300 def _validate_pattern(self, value):
5291 5301 re.compile('^' + glob2re(value) + '$')
5292 5302
5293 5303 @hybrid_property
5294 5304 def source_branch_pattern(self):
5295 5305 return self._branch_pattern or '*'
5296 5306
5297 5307 @source_branch_pattern.setter
5298 5308 def source_branch_pattern(self, value):
5299 5309 self._validate_pattern(value)
5300 5310 self._branch_pattern = value or '*'
5301 5311
5302 5312 @hybrid_property
5303 5313 def target_branch_pattern(self):
5304 5314 return self._target_branch_pattern or '*'
5305 5315
5306 5316 @target_branch_pattern.setter
5307 5317 def target_branch_pattern(self, value):
5308 5318 self._validate_pattern(value)
5309 5319 self._target_branch_pattern = value or '*'
5310 5320
5311 5321 @hybrid_property
5312 5322 def file_pattern(self):
5313 5323 return self._file_pattern or '*'
5314 5324
5315 5325 @file_pattern.setter
5316 5326 def file_pattern(self, value):
5317 5327 self._validate_pattern(value)
5318 5328 self._file_pattern = value or '*'
5319 5329
5320 5330 @hybrid_property
5321 5331 def forbid_pr_author_to_review(self):
5322 5332 return self.pr_author == 'forbid_pr_author'
5323 5333
5324 5334 @hybrid_property
5325 5335 def include_pr_author_to_review(self):
5326 5336 return self.pr_author == 'include_pr_author'
5327 5337
5328 5338 @hybrid_property
5329 5339 def forbid_commit_author_to_review(self):
5330 5340 return self.commit_author == 'forbid_commit_author'
5331 5341
5332 5342 @hybrid_property
5333 5343 def include_commit_author_to_review(self):
5334 5344 return self.commit_author == 'include_commit_author'
5335 5345
5336 5346 def matches(self, source_branch, target_branch, files_changed):
5337 5347 """
5338 5348 Check if this review rule matches a branch/files in a pull request
5339 5349
5340 5350 :param source_branch: source branch name for the commit
5341 5351 :param target_branch: target branch name for the commit
5342 5352 :param files_changed: list of file paths changed in the pull request
5343 5353 """
5344 5354
5345 5355 source_branch = source_branch or ''
5346 5356 target_branch = target_branch or ''
5347 5357 files_changed = files_changed or []
5348 5358
5349 5359 branch_matches = True
5350 5360 if source_branch or target_branch:
5351 5361 if self.source_branch_pattern == '*':
5352 5362 source_branch_match = True
5353 5363 else:
5354 5364 if self.source_branch_pattern.startswith('re:'):
5355 5365 source_pattern = self.source_branch_pattern[3:]
5356 5366 else:
5357 5367 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5358 5368 source_branch_regex = re.compile(source_pattern)
5359 5369 source_branch_match = bool(source_branch_regex.search(source_branch))
5360 5370 if self.target_branch_pattern == '*':
5361 5371 target_branch_match = True
5362 5372 else:
5363 5373 if self.target_branch_pattern.startswith('re:'):
5364 5374 target_pattern = self.target_branch_pattern[3:]
5365 5375 else:
5366 5376 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5367 5377 target_branch_regex = re.compile(target_pattern)
5368 5378 target_branch_match = bool(target_branch_regex.search(target_branch))
5369 5379
5370 5380 branch_matches = source_branch_match and target_branch_match
5371 5381
5372 5382 files_matches = True
5373 5383 if self.file_pattern != '*':
5374 5384 files_matches = False
5375 5385 if self.file_pattern.startswith('re:'):
5376 5386 file_pattern = self.file_pattern[3:]
5377 5387 else:
5378 5388 file_pattern = glob2re(self.file_pattern)
5379 5389 file_regex = re.compile(file_pattern)
5380 5390 for file_data in files_changed:
5381 5391 filename = file_data.get('filename')
5382 5392
5383 5393 if file_regex.search(filename):
5384 5394 files_matches = True
5385 5395 break
5386 5396
5387 5397 return branch_matches and files_matches
5388 5398
5389 5399 @property
5390 5400 def review_users(self):
5391 5401 """ Returns the users which this rule applies to """
5392 5402
5393 5403 users = collections.OrderedDict()
5394 5404
5395 5405 for rule_user in self.rule_users:
5396 5406 if rule_user.user.active:
5397 5407 if rule_user.user not in users:
5398 5408 users[rule_user.user.username] = {
5399 5409 'user': rule_user.user,
5400 5410 'source': 'user',
5401 5411 'source_data': {},
5402 5412 'data': rule_user.rule_data()
5403 5413 }
5404 5414
5405 5415 for rule_user_group in self.rule_user_groups:
5406 5416 source_data = {
5407 5417 'user_group_id': rule_user_group.users_group.users_group_id,
5408 5418 'name': rule_user_group.users_group.users_group_name,
5409 5419 'members': len(rule_user_group.users_group.members)
5410 5420 }
5411 5421 for member in rule_user_group.users_group.members:
5412 5422 if member.user.active:
5413 5423 key = member.user.username
5414 5424 if key in users:
5415 5425 # skip this member as we have him already
5416 5426 # this prevents from override the "first" matched
5417 5427 # users with duplicates in multiple groups
5418 5428 continue
5419 5429
5420 5430 users[key] = {
5421 5431 'user': member.user,
5422 5432 'source': 'user_group',
5423 5433 'source_data': source_data,
5424 5434 'data': rule_user_group.rule_data()
5425 5435 }
5426 5436
5427 5437 return users
5428 5438
5429 5439 def user_group_vote_rule(self, user_id):
5430 5440
5431 5441 rules = []
5432 5442 if not self.rule_user_groups:
5433 5443 return rules
5434 5444
5435 5445 for user_group in self.rule_user_groups:
5436 5446 user_group_members = [x.user_id for x in user_group.users_group.members]
5437 5447 if user_id in user_group_members:
5438 5448 rules.append(user_group)
5439 5449 return rules
5440 5450
5441 5451 def __repr__(self):
5442 5452 return f'<RepoReviewerRule(id={self.repo_review_rule_id}, repo={self.repo!r})>'
5443 5453
5444 5454
5445 5455 class ScheduleEntry(Base, BaseModel):
5446 5456 __tablename__ = 'schedule_entries'
5447 5457 __table_args__ = (
5448 5458 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5449 5459 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5450 5460 base_table_args,
5451 5461 )
5452 5462 SCHEDULE_TYPE_INTEGER = "integer"
5453 5463 SCHEDULE_TYPE_CRONTAB = "crontab"
5454 5464
5455 5465 schedule_types = [SCHEDULE_TYPE_CRONTAB, SCHEDULE_TYPE_INTEGER]
5456 5466 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5457 5467
5458 5468 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5459 5469 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5460 5470 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5461 5471
5462 5472 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5463 5473 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5464 5474
5465 5475 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5466 5476 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5467 5477
5468 5478 # task
5469 5479 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5470 5480 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5471 5481 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5472 5482 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5473 5483
5474 5484 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5475 5485 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5476 5486
5477 5487 @hybrid_property
5478 5488 def schedule_type(self):
5479 5489 return self._schedule_type
5480 5490
5481 5491 @schedule_type.setter
5482 5492 def schedule_type(self, val):
5483 5493 if val not in self.schedule_types:
5484 5494 raise ValueError(f'Value must be on of `{val}` and got `{self.schedule_type}`')
5485 5495
5486 5496 self._schedule_type = val
5487 5497
5488 5498 @classmethod
5489 5499 def get_uid(cls, obj):
5490 5500 args = obj.task_args
5491 5501 kwargs = obj.task_kwargs
5492 5502
5493 5503 if isinstance(args, JsonRaw):
5494 5504 try:
5495 5505 args = json.loads(str(args))
5496 5506 except ValueError:
5497 5507 log.exception('json.loads of args failed...')
5498 5508 args = tuple()
5499 5509
5500 5510 if isinstance(kwargs, JsonRaw):
5501 5511 try:
5502 5512 kwargs = json.loads(str(kwargs))
5503 5513 except ValueError:
5504 5514 log.exception('json.loads of kwargs failed...')
5505 5515 kwargs = dict()
5506 5516
5507 5517 dot_notation = obj.task_dot_notation
5508 5518 val = '.'.join(map(safe_str, [dot_notation, args, sorted(kwargs.items())]))
5509 5519 log.debug('calculating task uid using id:`%s`', val)
5510 5520
5511 5521 return sha1(safe_bytes(val))
5512 5522
5513 5523 @classmethod
5514 5524 def get_by_schedule_name(cls, schedule_name):
5515 5525 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5516 5526
5517 5527 @classmethod
5518 5528 def get_by_schedule_id(cls, schedule_id):
5519 5529 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5520 5530
5521 5531 @classmethod
5522 5532 def get_by_task_uid(cls, task_uid):
5523 5533 return cls.query().filter(cls.task_uid == task_uid).scalar()
5524 5534
5525 5535 @property
5526 5536 def task(self):
5527 5537 return self.task_dot_notation
5528 5538
5529 5539 @property
5530 5540 def schedule(self):
5531 5541 from rhodecode.lib.celerylib.utils import raw_2_schedule
5532 5542 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5533 5543 return schedule
5534 5544
5535 5545 @property
5536 5546 def args(self):
5537 5547 try:
5538 5548 return list(self.task_args or [])
5539 5549 except ValueError:
5540 5550 return list()
5541 5551
5542 5552 @property
5543 5553 def kwargs(self):
5544 5554 try:
5545 5555 return dict(self.task_kwargs or {})
5546 5556 except ValueError:
5547 5557 return dict()
5548 5558
5549 5559 def _as_raw(self, val, indent=False):
5550 5560 if hasattr(val, 'de_coerce'):
5551 5561 val = val.de_coerce()
5552 5562 if val:
5553 5563 if indent:
5554 5564 val = ext_json.formatted_str_json(val)
5555 5565 else:
5556 5566 val = ext_json.str_json(val)
5557 5567
5558 5568 return val
5559 5569
5560 5570 @property
5561 5571 def schedule_definition_raw(self):
5562 5572 return self._as_raw(self.schedule_definition)
5563 5573
5564 5574 def args_raw(self, indent=False):
5565 5575 return self._as_raw(self.task_args, indent)
5566 5576
5567 5577 def kwargs_raw(self, indent=False):
5568 5578 return self._as_raw(self.task_kwargs, indent)
5569 5579
5570 5580 def __repr__(self):
5571 5581 return f'<DB:ScheduleEntry({self.schedule_entry_id}:{self.schedule_name})>'
5572 5582
5573 5583
5574 5584 @event.listens_for(ScheduleEntry, 'before_update')
5575 5585 def update_task_uid(mapper, connection, target):
5576 5586 target.task_uid = ScheduleEntry.get_uid(target)
5577 5587
5578 5588
5579 5589 @event.listens_for(ScheduleEntry, 'before_insert')
5580 5590 def set_task_uid(mapper, connection, target):
5581 5591 target.task_uid = ScheduleEntry.get_uid(target)
5582 5592
5583 5593
5584 5594 class _BaseBranchPerms(BaseModel):
5585 5595 @classmethod
5586 5596 def compute_hash(cls, value):
5587 5597 return sha1_safe(value)
5588 5598
5589 5599 @hybrid_property
5590 5600 def branch_pattern(self):
5591 5601 return self._branch_pattern or '*'
5592 5602
5593 5603 @hybrid_property
5594 5604 def branch_hash(self):
5595 5605 return self._branch_hash
5596 5606
5597 5607 def _validate_glob(self, value):
5598 5608 re.compile('^' + glob2re(value) + '$')
5599 5609
5600 5610 @branch_pattern.setter
5601 5611 def branch_pattern(self, value):
5602 5612 self._validate_glob(value)
5603 5613 self._branch_pattern = value or '*'
5604 5614 # set the Hash when setting the branch pattern
5605 5615 self._branch_hash = self.compute_hash(self._branch_pattern)
5606 5616
5607 5617 def matches(self, branch):
5608 5618 """
5609 5619 Check if this the branch matches entry
5610 5620
5611 5621 :param branch: branch name for the commit
5612 5622 """
5613 5623
5614 5624 branch = branch or ''
5615 5625
5616 5626 branch_matches = True
5617 5627 if branch:
5618 5628 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5619 5629 branch_matches = bool(branch_regex.search(branch))
5620 5630
5621 5631 return branch_matches
5622 5632
5623 5633
5624 5634 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5625 5635 __tablename__ = 'user_to_repo_branch_permissions'
5626 5636 __table_args__ = (
5627 5637 base_table_args
5628 5638 )
5629 5639
5630 5640 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5631 5641
5632 5642 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5633 5643 repo = relationship('Repository', back_populates='user_branch_perms')
5634 5644
5635 5645 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5636 5646 permission = relationship('Permission')
5637 5647
5638 5648 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5639 5649 user_repo_to_perm = relationship('UserRepoToPerm', back_populates='branch_perm_entry')
5640 5650
5641 5651 rule_order = Column('rule_order', Integer(), nullable=False)
5642 5652 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5643 5653 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5644 5654
5645 5655 def __repr__(self):
5646 5656 return f'<UserBranchPermission({self.user_repo_to_perm} => {self.branch_pattern!r})>'
5647 5657
5648 5658
5649 5659 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5650 5660 __tablename__ = 'user_group_to_repo_branch_permissions'
5651 5661 __table_args__ = (
5652 5662 base_table_args
5653 5663 )
5654 5664
5655 5665 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5656 5666
5657 5667 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5658 5668 repo = relationship('Repository', back_populates='user_group_branch_perms')
5659 5669
5660 5670 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5661 5671 permission = relationship('Permission')
5662 5672
5663 5673 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None)
5664 5674 user_group_repo_to_perm = relationship('UserGroupRepoToPerm', back_populates='user_group_branch_perms')
5665 5675
5666 5676 rule_order = Column('rule_order', Integer(), nullable=False)
5667 5677 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5668 5678 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5669 5679
5670 5680 def __repr__(self):
5671 5681 return f'<UserBranchPermission({self.user_group_repo_to_perm} => {self.branch_pattern!r})>'
5672 5682
5673 5683
5674 5684 class UserBookmark(Base, BaseModel):
5675 5685 __tablename__ = 'user_bookmarks'
5676 5686 __table_args__ = (
5677 5687 UniqueConstraint('user_id', 'bookmark_repo_id'),
5678 5688 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5679 5689 UniqueConstraint('user_id', 'bookmark_position'),
5680 5690 base_table_args
5681 5691 )
5682 5692
5683 5693 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5684 5694 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5685 5695 position = Column("bookmark_position", Integer(), nullable=False)
5686 5696 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5687 5697 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5688 5698 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5689 5699
5690 5700 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5691 5701 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5692 5702
5693 5703 user = relationship("User")
5694 5704
5695 5705 repository = relationship("Repository")
5696 5706 repository_group = relationship("RepoGroup")
5697 5707
5698 5708 @classmethod
5699 5709 def get_by_position_for_user(cls, position, user_id):
5700 5710 return cls.query() \
5701 5711 .filter(UserBookmark.user_id == user_id) \
5702 5712 .filter(UserBookmark.position == position).scalar()
5703 5713
5704 5714 @classmethod
5705 5715 def get_bookmarks_for_user(cls, user_id, cache=True):
5706 5716 bookmarks = select(
5707 5717 UserBookmark.title,
5708 5718 UserBookmark.position,
5709 5719 ) \
5710 5720 .add_columns(Repository.repo_id, Repository.repo_type, Repository.repo_name) \
5711 5721 .add_columns(RepoGroup.group_id, RepoGroup.group_name) \
5712 5722 .where(UserBookmark.user_id == user_id) \
5713 5723 .outerjoin(Repository, Repository.repo_id == UserBookmark.bookmark_repo_id) \
5714 5724 .outerjoin(RepoGroup, RepoGroup.group_id == UserBookmark.bookmark_repo_group_id) \
5715 5725 .order_by(UserBookmark.position.asc())
5716 5726
5717 5727 if cache:
5718 5728 bookmarks = bookmarks.options(
5719 5729 FromCache("sql_cache_short", f"get_user_{user_id}_bookmarks")
5720 5730 )
5721 5731
5722 5732 return Session().execute(bookmarks).all()
5723 5733
5724 5734 def __repr__(self):
5725 5735 return f'<UserBookmark({self.position} @ {self.redirect_url!r})>'
5726 5736
5727 5737
5728 5738 class FileStore(Base, BaseModel):
5729 5739 __tablename__ = 'file_store'
5730 5740 __table_args__ = (
5731 5741 base_table_args
5732 5742 )
5733 5743
5734 5744 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5735 5745 file_uid = Column('file_uid', String(1024), nullable=False)
5736 5746 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5737 5747 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5738 5748 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5739 5749
5740 5750 # sha256 hash
5741 5751 file_hash = Column('file_hash', String(512), nullable=False)
5742 5752 file_size = Column('file_size', BigInteger(), nullable=False)
5743 5753
5744 5754 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5745 5755 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5746 5756 accessed_count = Column('accessed_count', Integer(), default=0)
5747 5757
5748 5758 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5749 5759
5750 5760 # if repo/repo_group reference is set, check for permissions
5751 5761 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5752 5762
5753 5763 # hidden defines an attachment that should be hidden from showing in artifact listing
5754 5764 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5755 5765
5756 5766 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5757 5767 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id', back_populates='artifacts')
5758 5768
5759 5769 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5760 5770
5761 5771 # scope limited to user, which requester have access to
5762 5772 scope_user_id = Column(
5763 5773 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5764 5774 nullable=True, unique=None, default=None)
5765 5775 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id', back_populates='scope_artifacts')
5766 5776
5767 5777 # scope limited to user group, which requester have access to
5768 5778 scope_user_group_id = Column(
5769 5779 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5770 5780 nullable=True, unique=None, default=None)
5771 5781 user_group = relationship('UserGroup', lazy='joined')
5772 5782
5773 5783 # scope limited to repo, which requester have access to
5774 5784 scope_repo_id = Column(
5775 5785 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5776 5786 nullable=True, unique=None, default=None)
5777 5787 repo = relationship('Repository', lazy='joined')
5778 5788
5779 5789 # scope limited to repo group, which requester have access to
5780 5790 scope_repo_group_id = Column(
5781 5791 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5782 5792 nullable=True, unique=None, default=None)
5783 5793 repo_group = relationship('RepoGroup', lazy='joined')
5784 5794
5785 5795 @classmethod
5786 5796 def get_scope(cls, scope_type, scope_id):
5787 5797 if scope_type == 'repo':
5788 5798 return f'repo:{scope_id}'
5789 5799 elif scope_type == 'repo-group':
5790 5800 return f'repo-group:{scope_id}'
5791 5801 elif scope_type == 'user':
5792 5802 return f'user:{scope_id}'
5793 5803 elif scope_type == 'user-group':
5794 5804 return f'user-group:{scope_id}'
5795 5805 else:
5796 5806 return scope_type
5797 5807
5798 5808 @classmethod
5799 5809 def get_by_store_uid(cls, file_store_uid, safe=False):
5800 5810 if safe:
5801 5811 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5802 5812 else:
5803 5813 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5804 5814
5805 5815 @classmethod
5806 5816 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5807 5817 file_description='', enabled=True, hidden=False, check_acl=True,
5808 5818 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5809 5819
5810 5820 store_entry = FileStore()
5811 5821 store_entry.file_uid = file_uid
5812 5822 store_entry.file_display_name = file_display_name
5813 5823 store_entry.file_org_name = filename
5814 5824 store_entry.file_size = file_size
5815 5825 store_entry.file_hash = file_hash
5816 5826 store_entry.file_description = file_description
5817 5827
5818 5828 store_entry.check_acl = check_acl
5819 5829 store_entry.enabled = enabled
5820 5830 store_entry.hidden = hidden
5821 5831
5822 5832 store_entry.user_id = user_id
5823 5833 store_entry.scope_user_id = scope_user_id
5824 5834 store_entry.scope_repo_id = scope_repo_id
5825 5835 store_entry.scope_repo_group_id = scope_repo_group_id
5826 5836
5827 5837 return store_entry
5828 5838
5829 5839 @classmethod
5830 5840 def store_metadata(cls, file_store_id, args, commit=True):
5831 5841 file_store = FileStore.get(file_store_id)
5832 5842 if file_store is None:
5833 5843 return
5834 5844
5835 5845 for section, key, value, value_type in args:
5836 5846 has_key = FileStoreMetadata().query() \
5837 5847 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5838 5848 .filter(FileStoreMetadata.file_store_meta_section == section) \
5839 5849 .filter(FileStoreMetadata.file_store_meta_key == key) \
5840 5850 .scalar()
5841 5851 if has_key:
5842 msg = 'key `{}` already defined under section `{}` for this file.'\
5843 .format(key, section)
5852 msg = f'key `{key}` already defined under section `{section}` for this file.'
5844 5853 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5845 5854
5846 5855 # NOTE(marcink): raises ArtifactMetadataBadValueType
5847 5856 FileStoreMetadata.valid_value_type(value_type)
5848 5857
5849 5858 meta_entry = FileStoreMetadata()
5850 5859 meta_entry.file_store = file_store
5851 5860 meta_entry.file_store_meta_section = section
5852 5861 meta_entry.file_store_meta_key = key
5853 5862 meta_entry.file_store_meta_value_type = value_type
5854 5863 meta_entry.file_store_meta_value = value
5855 5864
5856 5865 Session().add(meta_entry)
5857 5866
5858 5867 try:
5859 5868 if commit:
5860 5869 Session().commit()
5861 5870 except IntegrityError:
5862 5871 Session().rollback()
5863 5872 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5864 5873
5865 5874 @classmethod
5866 5875 def bump_access_counter(cls, file_uid, commit=True):
5867 5876 FileStore().query()\
5868 5877 .filter(FileStore.file_uid == file_uid)\
5869 5878 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5870 5879 FileStore.accessed_on: datetime.datetime.now()})
5871 5880 if commit:
5872 5881 Session().commit()
5873 5882
5874 5883 def __json__(self):
5875 5884 data = {
5876 5885 'filename': self.file_display_name,
5877 5886 'filename_org': self.file_org_name,
5878 5887 'file_uid': self.file_uid,
5879 5888 'description': self.file_description,
5880 5889 'hidden': self.hidden,
5881 5890 'size': self.file_size,
5882 5891 'created_on': self.created_on,
5883 5892 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5884 5893 'downloaded_times': self.accessed_count,
5885 5894 'sha256': self.file_hash,
5886 5895 'metadata': self.file_metadata,
5887 5896 }
5888 5897
5889 5898 return data
5890 5899
5891 5900 def __repr__(self):
5892 5901 return f'<FileStore({self.file_store_id})>'
5893 5902
5894 5903
5895 5904 class FileStoreMetadata(Base, BaseModel):
5896 5905 __tablename__ = 'file_store_metadata'
5897 5906 __table_args__ = (
5898 5907 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5899 5908 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5900 5909 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5901 5910 base_table_args
5902 5911 )
5903 5912 SETTINGS_TYPES = {
5904 5913 'str': safe_str,
5905 5914 'int': safe_int,
5906 5915 'unicode': safe_str,
5907 5916 'bool': str2bool,
5908 5917 'list': functools.partial(aslist, sep=',')
5909 5918 }
5910 5919
5911 5920 file_store_meta_id = Column(
5912 5921 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5913 5922 primary_key=True)
5914 5923 _file_store_meta_section = Column(
5915 5924 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5916 5925 nullable=True, unique=None, default=None)
5917 5926 _file_store_meta_section_hash = Column(
5918 5927 "file_store_meta_section_hash", String(255),
5919 5928 nullable=True, unique=None, default=None)
5920 5929 _file_store_meta_key = Column(
5921 5930 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5922 5931 nullable=True, unique=None, default=None)
5923 5932 _file_store_meta_key_hash = Column(
5924 5933 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5925 5934 _file_store_meta_value = Column(
5926 5935 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5927 5936 nullable=True, unique=None, default=None)
5928 5937 _file_store_meta_value_type = Column(
5929 5938 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5930 5939 default='unicode')
5931 5940
5932 5941 file_store_id = Column(
5933 5942 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5934 5943 nullable=True, unique=None, default=None)
5935 5944
5936 5945 file_store = relationship('FileStore', lazy='joined', viewonly=True)
5937 5946
5938 5947 @classmethod
5939 5948 def valid_value_type(cls, value):
5940 5949 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5941 5950 raise ArtifactMetadataBadValueType(
5942 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5951 f'value_type must be one of {cls.SETTINGS_TYPES.keys()} got {value}')
5943 5952
5944 5953 @hybrid_property
5945 5954 def file_store_meta_section(self):
5946 5955 return self._file_store_meta_section
5947 5956
5948 5957 @file_store_meta_section.setter
5949 5958 def file_store_meta_section(self, value):
5950 5959 self._file_store_meta_section = value
5951 5960 self._file_store_meta_section_hash = _hash_key(value)
5952 5961
5953 5962 @hybrid_property
5954 5963 def file_store_meta_key(self):
5955 5964 return self._file_store_meta_key
5956 5965
5957 5966 @file_store_meta_key.setter
5958 5967 def file_store_meta_key(self, value):
5959 5968 self._file_store_meta_key = value
5960 5969 self._file_store_meta_key_hash = _hash_key(value)
5961 5970
5962 5971 @hybrid_property
5963 5972 def file_store_meta_value(self):
5964 5973 val = self._file_store_meta_value
5965 5974
5966 5975 if self._file_store_meta_value_type:
5967 5976 # e.g unicode.encrypted == unicode
5968 5977 _type = self._file_store_meta_value_type.split('.')[0]
5969 5978 # decode the encrypted value if it's encrypted field type
5970 5979 if '.encrypted' in self._file_store_meta_value_type:
5971 5980 cipher = EncryptedTextValue()
5972 5981 val = safe_str(cipher.process_result_value(val, None))
5973 5982 # do final type conversion
5974 5983 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5975 5984 val = converter(val)
5976 5985
5977 5986 return val
5978 5987
5979 5988 @file_store_meta_value.setter
5980 5989 def file_store_meta_value(self, val):
5981 5990 val = safe_str(val)
5982 5991 # encode the encrypted value
5983 5992 if '.encrypted' in self.file_store_meta_value_type:
5984 5993 cipher = EncryptedTextValue()
5985 5994 val = safe_str(cipher.process_bind_param(val, None))
5986 5995 self._file_store_meta_value = val
5987 5996
5988 5997 @hybrid_property
5989 5998 def file_store_meta_value_type(self):
5990 5999 return self._file_store_meta_value_type
5991 6000
5992 6001 @file_store_meta_value_type.setter
5993 6002 def file_store_meta_value_type(self, val):
5994 6003 # e.g unicode.encrypted
5995 6004 self.valid_value_type(val)
5996 6005 self._file_store_meta_value_type = val
5997 6006
5998 6007 def __json__(self):
5999 6008 data = {
6000 6009 'artifact': self.file_store.file_uid,
6001 6010 'section': self.file_store_meta_section,
6002 6011 'key': self.file_store_meta_key,
6003 6012 'value': self.file_store_meta_value,
6004 6013 }
6005 6014
6006 6015 return data
6007 6016
6008 6017 def __repr__(self):
6009 6018 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.file_store_meta_section,
6010 6019 self.file_store_meta_key, self.file_store_meta_value)
6011 6020
6012 6021
6013 6022 class DbMigrateVersion(Base, BaseModel):
6014 6023 __tablename__ = 'db_migrate_version'
6015 6024 __table_args__ = (
6016 6025 base_table_args,
6017 6026 )
6018 6027
6019 6028 repository_id = Column('repository_id', String(250), primary_key=True)
6020 6029 repository_path = Column('repository_path', Text)
6021 6030 version = Column('version', Integer)
6022 6031
6023 6032 @classmethod
6024 6033 def set_version(cls, version):
6025 6034 """
6026 6035 Helper for forcing a different version, usually for debugging purposes via ishell.
6027 6036 """
6028 6037 ver = DbMigrateVersion.query().first()
6029 6038 ver.version = version
6030 6039 Session().commit()
6031 6040
6032 6041
6033 6042 class DbSession(Base, BaseModel):
6034 6043 __tablename__ = 'db_session'
6035 6044 __table_args__ = (
6036 6045 base_table_args,
6037 6046 )
6038 6047
6039 6048 def __repr__(self):
6040 6049 return f'<DB:DbSession({self.id})>'
6041 6050
6042 6051 id = Column('id', Integer())
6043 6052 namespace = Column('namespace', String(255), primary_key=True)
6044 6053 accessed = Column('accessed', DateTime, nullable=False)
6045 6054 created = Column('created', DateTime, nullable=False)
6046 6055 data = Column('data', PickleType, nullable=False)
@@ -1,655 +1,662 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 this is forms validation classes
21 21 http://formencode.org/module-formencode.validators.html
22 22 for list off all availible validators
23 23
24 24 we can create our own validators
25 25
26 26 The table below outlines the options which can be used in a schema in addition to the validators themselves
27 27 pre_validators [] These validators will be applied before the schema
28 28 chained_validators [] These validators will be applied after the schema
29 29 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
30 30 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
31 31 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
32 32 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
33 33
34 34
35 35 <name> = formencode.validators.<name of validator>
36 36 <name> must equal form name
37 37 list=[1,2,3,4,5]
38 38 for SELECT use formencode.All(OneOf(list), Int())
39 39
40 40 """
41 41
42 42 import deform
43 43 import logging
44 44 import formencode
45 45
46 46 from pkg_resources import resource_filename
47 47 from formencode import All, Pipe
48 48
49 49 from pyramid.threadlocal import get_current_request
50 50
51 51 from rhodecode import BACKENDS
52 52 from rhodecode.lib import helpers
53 53 from rhodecode.model import validators as v
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 deform_templates = resource_filename('deform', 'templates')
59 59 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
60 60 search_path = (rhodecode_templates, deform_templates)
61 61
62 62
63 63 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
64 64 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
65 65 def __call__(self, template_name, **kw):
66 66 kw['h'] = helpers
67 67 kw['request'] = get_current_request()
68 68 return self.load(template_name)(**kw)
69 69
70 70
71 71 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
72 72 deform.Form.set_default_renderer(form_renderer)
73 73
74 74
75 75 def LoginForm(localizer):
76 76 _ = localizer
77 77
78 78 class _LoginForm(formencode.Schema):
79 79 allow_extra_fields = True
80 80 filter_extra_fields = True
81 81 username = v.UnicodeString(
82 82 strip=True,
83 83 min=1,
84 84 not_empty=True,
85 85 messages={
86 86 'empty': _('Please enter a login'),
87 87 'tooShort': _('Enter a value %(min)i characters long or more')
88 88 }
89 89 )
90 90
91 91 password = v.UnicodeString(
92 92 strip=False,
93 93 min=3,
94 94 max=72,
95 95 not_empty=True,
96 96 messages={
97 97 'empty': _('Please enter a password'),
98 98 'tooShort': _('Enter %(min)i characters or more')}
99 99 )
100 100
101 101 remember = v.StringBoolean(if_missing=False)
102 102
103 103 chained_validators = [v.ValidAuth(localizer)]
104 104 return _LoginForm
105 105
106 106
107 107 def TOTPForm(localizer, user, allow_recovery_code_use=False):
108 108 _ = localizer
109 109
110 110 class _TOTPForm(formencode.Schema):
111 111 allow_extra_fields = True
112 112 filter_extra_fields = False
113 113 totp = v.Regex(r'^(?:\d{6}|[A-Z0-9]{32})$')
114 114 secret_totp = v.String()
115 115
116 116 def to_python(self, value, state=None):
117 117 validation_checks = [user.is_totp_valid]
118 118 if allow_recovery_code_use:
119 119 validation_checks.append(user.is_2fa_recovery_code_valid)
120 120 form_data = super().to_python(value, state)
121 121 received_code = form_data['totp']
122 122 secret = form_data.get('secret_totp')
123 123
124 124 if not any(map(lambda func: func(received_code, secret), validation_checks)):
125 125 error_msg = _('Code is invalid. Try again!')
126 126 raise formencode.Invalid(error_msg, v, state, error_dict={'totp': error_msg})
127 127 return form_data
128 128
129 129 return _TOTPForm
130 130
131 131
132 def WhitelistedVcsClientsForm(localizer):
133 _ = localizer
134
135 class _WhitelistedVcsClientsForm(formencode.Schema):
136 regexp = r'^(?:\s*[<>=~^!]*\s*\d{1,2}\.\d{1,2}(?:\.\d{1,2})?\s*|\*)\s*(?:,\s*[<>=~^!]*\s*\d{1,2}\.\d{1,2}(?:\.\d{1,2})?\s*|\s*\*\s*)*$'
137 allow_extra_fields = True
138 filter_extra_fields = True
139 git = v.Regex(regexp)
140 hg = v.Regex(regexp)
141 svn = v.Regex(regexp)
142
143 return _WhitelistedVcsClientsForm
144
145
132 146 def UserForm(localizer, edit=False, available_languages=None, old_data=None):
133 147 old_data = old_data or {}
134 148 available_languages = available_languages or []
135 149 _ = localizer
136 150
137 151 class _UserForm(formencode.Schema):
138 152 allow_extra_fields = True
139 153 filter_extra_fields = True
140 154 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
141 155 v.ValidUsername(localizer, edit, old_data))
142 156 if edit:
143 157 new_password = All(
144 158 v.ValidPassword(localizer),
145 159 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
146 160 )
147 161 password_confirmation = All(
148 162 v.ValidPassword(localizer),
149 163 v.UnicodeString(strip=False, min=6, max=72, not_empty=False),
150 164 )
151 165 admin = v.StringBoolean(if_missing=False)
152 166 else:
153 167 password = All(
154 168 v.ValidPassword(localizer),
155 169 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
156 170 )
157 171 password_confirmation = All(
158 172 v.ValidPassword(localizer),
159 173 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
160 174 )
161 175
162 176 password_change = v.StringBoolean(if_missing=False)
163 177 create_repo_group = v.StringBoolean(if_missing=False)
164 178
165 179 active = v.StringBoolean(if_missing=False)
166 180 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
167 181 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
168 182 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
169 183 description = v.UnicodeString(strip=True, min=1, max=250, not_empty=False,
170 184 if_missing='')
171 185 extern_name = v.UnicodeString(strip=True)
172 186 extern_type = v.UnicodeString(strip=True)
173 187 language = v.OneOf(available_languages, hideList=False,
174 188 testValueList=True, if_missing=None)
175 189 chained_validators = [v.ValidPasswordsMatch(localizer)]
176 190 return _UserForm
177 191
178 192
179 193 def UserGroupForm(localizer, edit=False, old_data=None, allow_disabled=False):
180 194 old_data = old_data or {}
181 195 _ = localizer
182 196
183 197 class _UserGroupForm(formencode.Schema):
184 198 allow_extra_fields = True
185 199 filter_extra_fields = True
186 200
187 201 users_group_name = All(
188 202 v.UnicodeString(strip=True, min=1, not_empty=True),
189 203 v.ValidUserGroup(localizer, edit, old_data)
190 204 )
191 205 user_group_description = v.UnicodeString(strip=True, min=1,
192 206 not_empty=False)
193 207
194 208 users_group_active = v.StringBoolean(if_missing=False)
195 209
196 210 if edit:
197 211 # this is user group owner
198 212 user = All(
199 213 v.UnicodeString(not_empty=True),
200 214 v.ValidRepoUser(localizer, allow_disabled))
201 215 return _UserGroupForm
202 216
203 217
204 218 def RepoGroupForm(localizer, edit=False, old_data=None, available_groups=None,
205 219 can_create_in_root=False, allow_disabled=False):
206 220 _ = localizer
207 221 old_data = old_data or {}
208 222 available_groups = available_groups or []
209 223
210 224 class _RepoGroupForm(formencode.Schema):
211 225 allow_extra_fields = True
212 226 filter_extra_fields = False
213 227
214 228 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
215 229 v.SlugifyName(localizer),)
216 230 group_description = v.UnicodeString(strip=True, min=1,
217 231 not_empty=False)
218 232 group_copy_permissions = v.StringBoolean(if_missing=False)
219 233
220 234 group_parent_id = v.OneOf(available_groups, hideList=False,
221 235 testValueList=True, not_empty=True)
222 236 enable_locking = v.StringBoolean(if_missing=False)
223 237 chained_validators = [
224 238 v.ValidRepoGroup(localizer, edit, old_data, can_create_in_root)]
225 239
226 240 if edit:
227 241 # this is repo group owner
228 242 user = All(
229 243 v.UnicodeString(not_empty=True),
230 244 v.ValidRepoUser(localizer, allow_disabled))
231 245 return _RepoGroupForm
232 246
233 247
234 248 def RegisterForm(localizer, edit=False, old_data=None):
235 249 _ = localizer
236 250 old_data = old_data or {}
237 251
238 252 class _RegisterForm(formencode.Schema):
239 253 allow_extra_fields = True
240 254 filter_extra_fields = True
241 255 username = All(
242 256 v.ValidUsername(localizer, edit, old_data),
243 257 v.UnicodeString(strip=True, min=1, not_empty=True)
244 258 )
245 259 password = All(
246 260 v.ValidPassword(localizer),
247 261 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
248 262 )
249 263 password_confirmation = All(
250 264 v.ValidPassword(localizer),
251 265 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
252 266 )
253 267 active = v.StringBoolean(if_missing=False)
254 268 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
255 269 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
256 270 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
257 271
258 272 chained_validators = [v.ValidPasswordsMatch(localizer)]
259 273 return _RegisterForm
260 274
261 275
262 276 def PasswordResetForm(localizer):
263 277 _ = localizer
264 278
265 279 class _PasswordResetForm(formencode.Schema):
266 280 allow_extra_fields = True
267 281 filter_extra_fields = True
268 282 email = All(v.ValidSystemEmail(localizer), v.Email(not_empty=True))
269 283 return _PasswordResetForm
270 284
271 285
272 286 def RepoForm(localizer, edit=False, old_data=None, repo_groups=None, allow_disabled=False):
273 287 _ = localizer
274 288 old_data = old_data or {}
275 289 repo_groups = repo_groups or []
276 290 supported_backends = BACKENDS.keys()
277 291
278 292 class _RepoForm(formencode.Schema):
279 293 allow_extra_fields = True
280 294 filter_extra_fields = False
281 295 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
282 296 v.SlugifyName(localizer), v.CannotHaveGitSuffix(localizer))
283 297 repo_group = All(v.CanWriteGroup(localizer, old_data),
284 298 v.OneOf(repo_groups, hideList=True))
285 299 repo_type = v.OneOf(supported_backends, required=False,
286 300 if_missing=old_data.get('repo_type'))
287 301 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
288 302 repo_private = v.StringBoolean(if_missing=False)
289 303 repo_copy_permissions = v.StringBoolean(if_missing=False)
290 304 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
291 305
292 306 repo_enable_statistics = v.StringBoolean(if_missing=False)
293 307 repo_enable_downloads = v.StringBoolean(if_missing=False)
294 308 repo_enable_locking = v.StringBoolean(if_missing=False)
295 309
296 310 if edit:
297 311 # this is repo owner
298 312 user = All(
299 313 v.UnicodeString(not_empty=True),
300 314 v.ValidRepoUser(localizer, allow_disabled))
301 315 clone_uri_change = v.UnicodeString(
302 316 not_empty=False, if_missing=v.Missing)
303 317
304 318 chained_validators = [v.ValidCloneUri(localizer),
305 319 v.ValidRepoName(localizer, edit, old_data)]
306 320 return _RepoForm
307 321
308 322
309 323 def RepoPermsForm(localizer):
310 324 _ = localizer
311 325
312 326 class _RepoPermsForm(formencode.Schema):
313 327 allow_extra_fields = True
314 328 filter_extra_fields = False
315 329 chained_validators = [v.ValidPerms(localizer, type_='repo')]
316 330 return _RepoPermsForm
317 331
318 332
319 333 def RepoGroupPermsForm(localizer, valid_recursive_choices):
320 334 _ = localizer
321 335
322 336 class _RepoGroupPermsForm(formencode.Schema):
323 337 allow_extra_fields = True
324 338 filter_extra_fields = False
325 339 recursive = v.OneOf(valid_recursive_choices)
326 340 chained_validators = [v.ValidPerms(localizer, type_='repo_group')]
327 341 return _RepoGroupPermsForm
328 342
329 343
330 344 def UserGroupPermsForm(localizer):
331 345 _ = localizer
332 346
333 347 class _UserPermsForm(formencode.Schema):
334 348 allow_extra_fields = True
335 349 filter_extra_fields = False
336 350 chained_validators = [v.ValidPerms(localizer, type_='user_group')]
337 351 return _UserPermsForm
338 352
339 353
340 354 def RepoFieldForm(localizer):
341 355 _ = localizer
342 356
343 357 class _RepoFieldForm(formencode.Schema):
344 358 filter_extra_fields = True
345 359 allow_extra_fields = True
346 360
347 361 new_field_key = All(v.FieldKey(localizer),
348 362 v.UnicodeString(strip=True, min=3, not_empty=True))
349 363 new_field_value = v.UnicodeString(not_empty=False, if_missing='')
350 364 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
351 365 if_missing='str')
352 366 new_field_label = v.UnicodeString(not_empty=False)
353 367 new_field_desc = v.UnicodeString(not_empty=False)
354 368 return _RepoFieldForm
355 369
356 370
357 371 def RepoForkForm(localizer, edit=False, old_data=None,
358 372 supported_backends=BACKENDS.keys(), repo_groups=None):
359 373 _ = localizer
360 374 old_data = old_data or {}
361 375 repo_groups = repo_groups or []
362 376
363 377 class _RepoForkForm(formencode.Schema):
364 378 allow_extra_fields = True
365 379 filter_extra_fields = False
366 380 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
367 381 v.SlugifyName(localizer))
368 382 repo_group = All(v.CanWriteGroup(localizer, ),
369 383 v.OneOf(repo_groups, hideList=True))
370 384 repo_type = All(v.ValidForkType(localizer, old_data), v.OneOf(supported_backends))
371 385 description = v.UnicodeString(strip=True, min=1, not_empty=True)
372 386 private = v.StringBoolean(if_missing=False)
373 387 copy_permissions = v.StringBoolean(if_missing=False)
374 388 fork_parent_id = v.UnicodeString()
375 389 chained_validators = [v.ValidForkName(localizer, edit, old_data)]
376 390 return _RepoForkForm
377 391
378 392
379 393 def ApplicationSettingsForm(localizer):
380 394 _ = localizer
381 395
382 396 class _ApplicationSettingsForm(formencode.Schema):
383 397 allow_extra_fields = True
384 398 filter_extra_fields = False
385 399 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
386 400 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
387 401 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
388 402 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
389 403 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
390 404 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
391 405 rhodecode_create_personal_repo_group = v.StringBoolean(if_missing=False)
392 406 rhodecode_personal_repo_group_pattern = v.UnicodeString(strip=True, min=1, not_empty=False)
393 407 return _ApplicationSettingsForm
394 408
395 409
396 410 def ApplicationVisualisationForm(localizer):
397 411 from rhodecode.model.db import Repository
398 412 _ = localizer
399 413
400 414 class _ApplicationVisualisationForm(formencode.Schema):
401 415 allow_extra_fields = True
402 416 filter_extra_fields = False
403 417 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
404 418 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
405 419 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
406 420
407 421 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
408 422 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
409 423 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
410 424 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
411 425 rhodecode_show_version = v.StringBoolean(if_missing=False)
412 426 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
413 427 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
414 428 rhodecode_gravatar_url = v.UnicodeString(min=3)
415 429 rhodecode_clone_uri_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI)
416 430 rhodecode_clone_uri_id_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_ID)
417 431 rhodecode_clone_uri_ssh_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_SSH)
418 432 rhodecode_support_url = v.UnicodeString()
419 433 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
420 434 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
421 435 return _ApplicationVisualisationForm
422 436
423 437
424 438 class _BaseVcsSettingsForm(formencode.Schema):
425 439
426 440 allow_extra_fields = True
427 441 filter_extra_fields = False
428 442 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
429 443 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
430 444 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
431 445
432 446 # PR/Code-review
433 447 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
434 448 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
435 449
436 450 # hg
437 451 extensions_largefiles = v.StringBoolean(if_missing=False)
438 452 extensions_evolve = v.StringBoolean(if_missing=False)
439 453 phases_publish = v.StringBoolean(if_missing=False)
440 454
441 455 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
442 456 rhodecode_hg_close_branch_before_merging = v.StringBoolean(if_missing=False)
443 457
444 458 # git
445 459 vcs_git_lfs_enabled = v.StringBoolean(if_missing=False)
446 460 rhodecode_git_use_rebase_for_merging = v.StringBoolean(if_missing=False)
447 461 rhodecode_git_close_branch_before_merging = v.StringBoolean(if_missing=False)
448 462
449 463 # cache
450 464 rhodecode_diff_cache = v.StringBoolean(if_missing=False)
451 465
452 466
453 467 def ApplicationUiSettingsForm(localizer):
454 468 _ = localizer
455 469
456 470 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
457 web_push_ssl = v.StringBoolean(if_missing=False)
458 largefiles_usercache = All(
459 v.ValidPath(localizer),
460 v.UnicodeString(strip=True, min=2, not_empty=True))
461 vcs_git_lfs_store_location = All(
462 v.ValidPath(localizer),
463 v.UnicodeString(strip=True, min=2, not_empty=True))
464 471 extensions_hggit = v.StringBoolean(if_missing=False)
465 472 new_svn_branch = v.ValidSvnPattern(localizer, section='vcs_svn_branch')
466 473 new_svn_tag = v.ValidSvnPattern(localizer, section='vcs_svn_tag')
467 474 return _ApplicationUiSettingsForm
468 475
469 476
470 477 def RepoVcsSettingsForm(localizer, repo_name):
471 478 _ = localizer
472 479
473 480 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
474 481 inherit_global_settings = v.StringBoolean(if_missing=False)
475 482 new_svn_branch = v.ValidSvnPattern(localizer,
476 483 section='vcs_svn_branch', repo_name=repo_name)
477 484 new_svn_tag = v.ValidSvnPattern(localizer,
478 485 section='vcs_svn_tag', repo_name=repo_name)
479 486 return _RepoVcsSettingsForm
480 487
481 488
482 489 def LabsSettingsForm(localizer):
483 490 _ = localizer
484 491
485 492 class _LabSettingsForm(formencode.Schema):
486 493 allow_extra_fields = True
487 494 filter_extra_fields = False
488 495 return _LabSettingsForm
489 496
490 497
491 498 def ApplicationPermissionsForm(
492 499 localizer, register_choices, password_reset_choices,
493 500 extern_activate_choices):
494 501 _ = localizer
495 502
496 503 class _DefaultPermissionsForm(formencode.Schema):
497 504 allow_extra_fields = True
498 505 filter_extra_fields = True
499 506
500 507 anonymous = v.StringBoolean(if_missing=False)
501 508 default_register = v.OneOf(register_choices)
502 509 default_register_message = v.UnicodeString()
503 510 default_password_reset = v.OneOf(password_reset_choices)
504 511 default_extern_activate = v.OneOf(extern_activate_choices)
505 512 return _DefaultPermissionsForm
506 513
507 514
508 515 def ObjectPermissionsForm(localizer, repo_perms_choices, group_perms_choices,
509 516 user_group_perms_choices):
510 517 _ = localizer
511 518
512 519 class _ObjectPermissionsForm(formencode.Schema):
513 520 allow_extra_fields = True
514 521 filter_extra_fields = True
515 522 overwrite_default_repo = v.StringBoolean(if_missing=False)
516 523 overwrite_default_group = v.StringBoolean(if_missing=False)
517 524 overwrite_default_user_group = v.StringBoolean(if_missing=False)
518 525
519 526 default_repo_perm = v.OneOf(repo_perms_choices)
520 527 default_group_perm = v.OneOf(group_perms_choices)
521 528 default_user_group_perm = v.OneOf(user_group_perms_choices)
522 529
523 530 return _ObjectPermissionsForm
524 531
525 532
526 533 def BranchPermissionsForm(localizer, branch_perms_choices):
527 534 _ = localizer
528 535
529 536 class _BranchPermissionsForm(formencode.Schema):
530 537 allow_extra_fields = True
531 538 filter_extra_fields = True
532 539 overwrite_default_branch = v.StringBoolean(if_missing=False)
533 540 default_branch_perm = v.OneOf(branch_perms_choices)
534 541
535 542 return _BranchPermissionsForm
536 543
537 544
538 545 def UserPermissionsForm(localizer, create_choices, create_on_write_choices,
539 546 repo_group_create_choices, user_group_create_choices,
540 547 fork_choices, inherit_default_permissions_choices):
541 548 _ = localizer
542 549
543 550 class _DefaultPermissionsForm(formencode.Schema):
544 551 allow_extra_fields = True
545 552 filter_extra_fields = True
546 553
547 554 anonymous = v.StringBoolean(if_missing=False)
548 555
549 556 default_repo_create = v.OneOf(create_choices)
550 557 default_repo_create_on_write = v.OneOf(create_on_write_choices)
551 558 default_user_group_create = v.OneOf(user_group_create_choices)
552 559 default_repo_group_create = v.OneOf(repo_group_create_choices)
553 560 default_fork_create = v.OneOf(fork_choices)
554 561 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
555 562 return _DefaultPermissionsForm
556 563
557 564
558 565 def UserIndividualPermissionsForm(localizer):
559 566 _ = localizer
560 567
561 568 class _DefaultPermissionsForm(formencode.Schema):
562 569 allow_extra_fields = True
563 570 filter_extra_fields = True
564 571
565 572 inherit_default_permissions = v.StringBoolean(if_missing=False)
566 573 return _DefaultPermissionsForm
567 574
568 575
569 576 def DefaultsForm(localizer, edit=False, old_data=None, supported_backends=BACKENDS.keys()):
570 577 _ = localizer
571 578 old_data = old_data or {}
572 579
573 580 class _DefaultsForm(formencode.Schema):
574 581 allow_extra_fields = True
575 582 filter_extra_fields = True
576 583 default_repo_type = v.OneOf(supported_backends)
577 584 default_repo_private = v.StringBoolean(if_missing=False)
578 585 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
579 586 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
580 587 default_repo_enable_locking = v.StringBoolean(if_missing=False)
581 588 return _DefaultsForm
582 589
583 590
584 591 def AuthSettingsForm(localizer):
585 592 _ = localizer
586 593
587 594 class _AuthSettingsForm(formencode.Schema):
588 595 allow_extra_fields = True
589 596 filter_extra_fields = True
590 597 auth_plugins = All(v.ValidAuthPlugins(localizer),
591 598 v.UniqueListFromString(localizer)(not_empty=True))
592 599 return _AuthSettingsForm
593 600
594 601
595 602 def UserExtraEmailForm(localizer):
596 603 _ = localizer
597 604
598 605 class _UserExtraEmailForm(formencode.Schema):
599 606 email = All(v.UniqSystemEmail(localizer), v.Email(not_empty=True))
600 607 return _UserExtraEmailForm
601 608
602 609
603 610 def UserExtraIpForm(localizer):
604 611 _ = localizer
605 612
606 613 class _UserExtraIpForm(formencode.Schema):
607 614 ip = v.ValidIp(localizer)(not_empty=True)
608 615 return _UserExtraIpForm
609 616
610 617
611 618 def PullRequestForm(localizer, repo_id):
612 619 _ = localizer
613 620
614 621 class ReviewerForm(formencode.Schema):
615 622 user_id = v.Int(not_empty=True)
616 623 reasons = All()
617 624 rules = All(v.UniqueList(localizer, convert=int)())
618 625 mandatory = v.StringBoolean()
619 626 role = v.String(if_missing='reviewer')
620 627
621 628 class ObserverForm(formencode.Schema):
622 629 user_id = v.Int(not_empty=True)
623 630 reasons = All()
624 631 rules = All(v.UniqueList(localizer, convert=int)())
625 632 mandatory = v.StringBoolean()
626 633 role = v.String(if_missing='observer')
627 634
628 635 class _PullRequestForm(formencode.Schema):
629 636 allow_extra_fields = True
630 637 filter_extra_fields = True
631 638
632 639 common_ancestor = v.UnicodeString(strip=True, required=True)
633 640 source_repo = v.UnicodeString(strip=True, required=True)
634 641 source_ref = v.UnicodeString(strip=True, required=True)
635 642 target_repo = v.UnicodeString(strip=True, required=True)
636 643 target_ref = v.UnicodeString(strip=True, required=True)
637 644 revisions = All(#v.NotReviewedRevisions(localizer, repo_id)(),
638 645 v.UniqueList(localizer)(not_empty=True))
639 646 review_members = formencode.ForEach(ReviewerForm())
640 647 observer_members = formencode.ForEach(ObserverForm())
641 648 pullrequest_title = v.UnicodeString(strip=True, required=True, min=1, max=255)
642 649 pullrequest_desc = v.UnicodeString(strip=True, required=False)
643 650 description_renderer = v.UnicodeString(strip=True, required=False)
644 651
645 652 return _PullRequestForm
646 653
647 654
648 655 def IssueTrackerPatternsForm(localizer):
649 656 _ = localizer
650 657
651 658 class _IssueTrackerPatternsForm(formencode.Schema):
652 659 allow_extra_fields = True
653 660 filter_extra_fields = False
654 661 chained_validators = [v.ValidPattern(localizer)]
655 662 return _IssueTrackerPatternsForm
@@ -1,456 +1,458 b''
1 1 # Copyright (C) 2011-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 """
21 21 Model for notifications
22 22 """
23 23
24 24 import logging
25 25 import traceback
26 26
27 27 import premailer
28 28 from pyramid.threadlocal import get_current_request
29 29 from sqlalchemy.sql.expression import false, true
30 30
31 31 import rhodecode
32 32 from rhodecode.lib import helpers as h
33 33 from rhodecode.model import BaseModel
34 34 from rhodecode.model.db import Notification, User, UserNotification
35 35 from rhodecode.model.meta import Session
36 36 from rhodecode.translation import TranslationString
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 class NotificationModel(BaseModel):
42 42
43 43 cls = Notification
44 44
45 45 def __get_notification(self, notification):
46 46 if isinstance(notification, Notification):
47 47 return notification
48 48 elif isinstance(notification, int):
49 49 return Notification.get(notification)
50 50 else:
51 51 if notification:
52 52 raise Exception('notification must be int or Instance'
53 53 ' of Notification got %s' % type(notification))
54 54
55 55 def create(
56 56 self, created_by, notification_subject='', notification_body='',
57 57 notification_type=Notification.TYPE_MESSAGE, recipients=None,
58 58 mention_recipients=None, with_email=True, email_kwargs=None):
59 59 """
60 60
61 61 Creates notification of given type
62 62
63 63 :param created_by: int, str or User instance. User who created this
64 64 notification
65 65 :param notification_subject: subject of notification itself,
66 66 it will be generated automatically from notification_type if not specified
67 67 :param notification_body: body of notification text
68 68 it will be generated automatically from notification_type if not specified
69 69 :param notification_type: type of notification, based on that we
70 70 pick templates
71 71 :param recipients: list of int, str or User objects, when None
72 72 is given send to all admins
73 73 :param mention_recipients: list of int, str or User objects,
74 74 that were mentioned
75 75 :param with_email: send email with this notification
76 76 :param email_kwargs: dict with arguments to generate email
77 77 """
78 78
79 79 from rhodecode.lib.celerylib import tasks, run_task
80 80
81 81 if recipients and not getattr(recipients, '__iter__', False):
82 82 raise Exception('recipients must be an iterable object')
83 83
84 84 if not (notification_subject and notification_body) and not notification_type:
85 85 raise ValueError('notification_subject, and notification_body '
86 86 'cannot be empty when notification_type is not specified')
87 87
88 88 created_by_obj = self._get_user(created_by)
89 89
90 90 if not created_by_obj:
91 91 raise Exception('unknown user %s' % created_by)
92 92
93 93 # default MAIN body if not given
94 94 email_kwargs = email_kwargs or {'body': notification_body}
95 95 mention_recipients = mention_recipients or set()
96 96
97 97 if recipients is None:
98 98 # recipients is None means to all admins
99 99 recipients_objs = User.query().filter(User.admin == true()).all()
100 100 log.debug('sending notifications %s to admins: %s',
101 101 notification_type, recipients_objs)
102 102 else:
103 103 recipients_objs = set()
104 104 for u in recipients:
105 105 obj = self._get_user(u)
106 106 if obj:
107 107 recipients_objs.add(obj)
108 108 else: # we didn't find this user, log the error and carry on
109 109 log.error('cannot notify unknown user %r', u)
110 110
111 111 if not recipients_objs:
112 112 raise Exception('no valid recipients specified')
113 113
114 114 log.debug('sending notifications %s to %s',
115 115 notification_type, recipients_objs)
116 116
117 117 # add mentioned users into recipients
118 118 final_recipients = set(recipients_objs).union(mention_recipients)
119 119
120 (subject, email_body, email_body_plaintext) = \
121 EmailNotificationModel().render_email(notification_type, **email_kwargs)
120 # No need to render email if we are sending just notification
121 if with_email:
122 (subject, email_body, email_body_plaintext) = \
123 EmailNotificationModel().render_email(notification_type, **email_kwargs)
122 124
123 if not notification_subject:
124 notification_subject = subject
125 if not notification_subject:
126 notification_subject = subject
125 127
126 if not notification_body:
127 notification_body = email_body_plaintext
128 if not notification_body:
129 notification_body = email_body_plaintext
128 130
129 131 notification = Notification.create(
130 132 created_by=created_by_obj, subject=notification_subject,
131 133 body=notification_body, recipients=final_recipients,
132 134 type_=notification_type
133 135 )
134 136
135 137 if not with_email: # skip sending email, and just create notification
136 138 return notification
137 139
138 140 # don't send email to person who created this comment
139 141 rec_objs = set(recipients_objs).difference({created_by_obj})
140 142
141 143 # now notify all recipients in question
142 144
143 145 for recipient in rec_objs.union(mention_recipients):
144 146 # inject current recipient
145 147 email_kwargs['recipient'] = recipient
146 148 email_kwargs['mention'] = recipient in mention_recipients
147 149 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
148 150 notification_type, **email_kwargs)
149 151
150 152 extra_headers = None
151 153 if 'thread_ids' in email_kwargs:
152 154 extra_headers = {'thread_ids': email_kwargs.pop('thread_ids')}
153 155
154 156 log.debug('Creating notification email task for user:`%s`', recipient)
155 157 task = run_task(tasks.send_email, recipient.email, subject,
156 158 email_body_plaintext, email_body, extra_headers=extra_headers)
157 159 log.debug('Created email task: %s', task)
158 160
159 161 return notification
160 162
161 163 def delete(self, user, notification):
162 164 # we don't want to remove actual notification just the assignment
163 165 try:
164 166 notification = self.__get_notification(notification)
165 167 user = self._get_user(user)
166 168 if notification and user:
167 169 obj = UserNotification.query()\
168 170 .filter(UserNotification.user == user)\
169 171 .filter(UserNotification.notification == notification)\
170 172 .one()
171 173 Session().delete(obj)
172 174 return True
173 175 except Exception:
174 176 log.error(traceback.format_exc())
175 177 raise
176 178
177 179 def get_for_user(self, user, filter_=None):
178 180 """
179 181 Get mentions for given user, filter them if filter dict is given
180 182 """
181 183 user = self._get_user(user)
182 184
183 185 q = UserNotification.query()\
184 186 .filter(UserNotification.user == user)\
185 187 .join((
186 188 Notification, UserNotification.notification_id ==
187 189 Notification.notification_id))
188 190 if filter_ == ['all']:
189 191 q = q # no filter
190 192 elif filter_ == ['unread']:
191 193 q = q.filter(UserNotification.read == false())
192 194 elif filter_:
193 195 q = q.filter(Notification.type_.in_(filter_))
194 196
195 197 q = q.order_by(Notification.created_on.desc())
196 198 return q
197 199
198 200 def mark_read(self, user, notification):
199 201 try:
200 202 notification = self.__get_notification(notification)
201 203 user = self._get_user(user)
202 204 if notification and user:
203 205 obj = UserNotification.query()\
204 206 .filter(UserNotification.user == user)\
205 207 .filter(UserNotification.notification == notification)\
206 208 .one()
207 209 obj.read = True
208 210 Session().add(obj)
209 211 return True
210 212 except Exception:
211 213 log.error(traceback.format_exc())
212 214 raise
213 215
214 216 def mark_all_read_for_user(self, user, filter_=None):
215 217 user = self._get_user(user)
216 218 q = UserNotification.query()\
217 219 .filter(UserNotification.user == user)\
218 220 .filter(UserNotification.read == false())\
219 221 .join((
220 222 Notification, UserNotification.notification_id ==
221 223 Notification.notification_id))
222 224 if filter_ == ['unread']:
223 225 q = q.filter(UserNotification.read == false())
224 226 elif filter_:
225 227 q = q.filter(Notification.type_.in_(filter_))
226 228
227 229 # this is a little inefficient but sqlalchemy doesn't support
228 230 # update on joined tables :(
229 231 for obj in q.all():
230 232 obj.read = True
231 233 Session().add(obj)
232 234
233 235 def get_unread_cnt_for_user(self, user):
234 236 user = self._get_user(user)
235 237 return UserNotification.query()\
236 238 .filter(UserNotification.read == false())\
237 239 .filter(UserNotification.user == user).count()
238 240
239 241 def get_unread_for_user(self, user):
240 242 user = self._get_user(user)
241 243 return [x.notification for x in UserNotification.query()
242 244 .filter(UserNotification.read == false())
243 245 .filter(UserNotification.user == user).all()]
244 246
245 247 def get_user_notification(self, user, notification):
246 248 user = self._get_user(user)
247 249 notification = self.__get_notification(notification)
248 250
249 251 return UserNotification.query()\
250 252 .filter(UserNotification.notification == notification)\
251 253 .filter(UserNotification.user == user).scalar()
252 254
253 255 def make_description(self, notification, translate, show_age=True):
254 256 """
255 257 Creates a human readable description based on properties
256 258 of notification object
257 259 """
258 260 _ = translate
259 261 _map = {
260 262 notification.TYPE_CHANGESET_COMMENT: [
261 263 _('%(user)s commented on commit %(date_or_age)s'),
262 264 _('%(user)s commented on commit at %(date_or_age)s'),
263 265 ],
264 266 notification.TYPE_MESSAGE: [
265 267 _('%(user)s sent message %(date_or_age)s'),
266 268 _('%(user)s sent message at %(date_or_age)s'),
267 269 ],
268 270 notification.TYPE_MENTION: [
269 271 _('%(user)s mentioned you %(date_or_age)s'),
270 272 _('%(user)s mentioned you at %(date_or_age)s'),
271 273 ],
272 274 notification.TYPE_REGISTRATION: [
273 275 _('%(user)s registered in RhodeCode %(date_or_age)s'),
274 276 _('%(user)s registered in RhodeCode at %(date_or_age)s'),
275 277 ],
276 278 notification.TYPE_PULL_REQUEST: [
277 279 _('%(user)s opened new pull request %(date_or_age)s'),
278 280 _('%(user)s opened new pull request at %(date_or_age)s'),
279 281 ],
280 282 notification.TYPE_PULL_REQUEST_UPDATE: [
281 283 _('%(user)s updated pull request %(date_or_age)s'),
282 284 _('%(user)s updated pull request at %(date_or_age)s'),
283 285 ],
284 286 notification.TYPE_PULL_REQUEST_COMMENT: [
285 287 _('%(user)s commented on pull request %(date_or_age)s'),
286 288 _('%(user)s commented on pull request at %(date_or_age)s'),
287 289 ],
288 290 }
289 291
290 292 templates = _map[notification.type_]
291 293
292 294 if show_age:
293 295 template = templates[0]
294 296 date_or_age = h.age(notification.created_on)
295 297 if translate:
296 298 date_or_age = translate(date_or_age)
297 299
298 300 if isinstance(date_or_age, TranslationString):
299 301 date_or_age = date_or_age.interpolate()
300 302
301 303 else:
302 304 template = templates[1]
303 305 date_or_age = h.format_date(notification.created_on)
304 306
305 307 return template % {
306 308 'user': notification.created_by_user.username,
307 309 'date_or_age': date_or_age,
308 310 }
309 311
310 312
311 313 # Templates for Titles, that could be overwritten by rcextensions
312 314 # Title of email for pull-request update
313 315 EMAIL_PR_UPDATE_SUBJECT_TEMPLATE = ''
314 316 # Title of email for request for pull request review
315 317 EMAIL_PR_REVIEW_SUBJECT_TEMPLATE = ''
316 318
317 319 # Title of email for general comment on pull request
318 320 EMAIL_PR_COMMENT_SUBJECT_TEMPLATE = ''
319 321 # Title of email for general comment which includes status change on pull request
320 322 EMAIL_PR_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = ''
321 323 # Title of email for inline comment on a file in pull request
322 324 EMAIL_PR_COMMENT_FILE_SUBJECT_TEMPLATE = ''
323 325
324 326 # Title of email for general comment on commit
325 327 EMAIL_COMMENT_SUBJECT_TEMPLATE = ''
326 328 # Title of email for general comment which includes status change on commit
327 329 EMAIL_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = ''
328 330 # Title of email for inline comment on a file in commit
329 331 EMAIL_COMMENT_FILE_SUBJECT_TEMPLATE = ''
330 332
331 333 import cssutils
332 334 # hijack css utils logger and replace with ours
333 335 log = logging.getLogger('rhodecode.cssutils.premailer')
334 336 log.setLevel(logging.INFO)
335 337 cssutils.log.setLog(log)
336 338
337 339
338 340 class EmailNotificationModel(BaseModel):
339 341 TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
340 342 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
341 343 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
342 344 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
343 345 TYPE_PULL_REQUEST_UPDATE = Notification.TYPE_PULL_REQUEST_UPDATE
344 346 TYPE_MAIN = Notification.TYPE_MESSAGE
345 347
346 348 TYPE_PASSWORD_RESET = 'password_reset'
347 349 TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation'
348 350 TYPE_EMAIL_TEST = 'email_test'
349 351 TYPE_EMAIL_EXCEPTION = 'exception'
350 352 TYPE_UPDATE_AVAILABLE = 'update_available'
351 353 TYPE_TEST = 'test'
352 354
353 355 email_types = {
354 356 TYPE_MAIN:
355 357 'rhodecode:templates/email_templates/main.mako',
356 358 TYPE_TEST:
357 359 'rhodecode:templates/email_templates/test.mako',
358 360 TYPE_EMAIL_EXCEPTION:
359 361 'rhodecode:templates/email_templates/exception_tracker.mako',
360 362 TYPE_UPDATE_AVAILABLE:
361 363 'rhodecode:templates/email_templates/update_available.mako',
362 364 TYPE_EMAIL_TEST:
363 365 'rhodecode:templates/email_templates/email_test.mako',
364 366 TYPE_REGISTRATION:
365 367 'rhodecode:templates/email_templates/user_registration.mako',
366 368 TYPE_PASSWORD_RESET:
367 369 'rhodecode:templates/email_templates/password_reset.mako',
368 370 TYPE_PASSWORD_RESET_CONFIRMATION:
369 371 'rhodecode:templates/email_templates/password_reset_confirmation.mako',
370 372 TYPE_COMMIT_COMMENT:
371 373 'rhodecode:templates/email_templates/commit_comment.mako',
372 374 TYPE_PULL_REQUEST:
373 375 'rhodecode:templates/email_templates/pull_request_review.mako',
374 376 TYPE_PULL_REQUEST_COMMENT:
375 377 'rhodecode:templates/email_templates/pull_request_comment.mako',
376 378 TYPE_PULL_REQUEST_UPDATE:
377 379 'rhodecode:templates/email_templates/pull_request_update.mako',
378 380 }
379 381
380 382 premailer_instance = premailer.Premailer(
381 383 #cssutils_logging_handler=log.handlers[0],
382 384 #cssutils_logging_level=logging.INFO
383 385 )
384 386
385 387 def __init__(self):
386 388 """
387 389 Example usage::
388 390
389 391 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
390 392 EmailNotificationModel.TYPE_TEST, **email_kwargs)
391 393
392 394 """
393 395 super().__init__()
394 396 self.rhodecode_instance_name = rhodecode.CONFIG.get('rhodecode_title')
395 397
396 398 def _update_kwargs_for_render(self, kwargs):
397 399 """
398 400 Inject params required for Mako rendering
399 401
400 402 :param kwargs:
401 403 """
402 404
403 405 kwargs['rhodecode_instance_name'] = self.rhodecode_instance_name
404 406 kwargs['rhodecode_version'] = rhodecode.__version__
405 407 instance_url = h.route_url('home')
406 408 _kwargs = {
407 409 'instance_url': instance_url,
408 410 'whitespace_filter': self.whitespace_filter,
409 411 'email_pr_update_subject_template': EMAIL_PR_UPDATE_SUBJECT_TEMPLATE,
410 412 'email_pr_review_subject_template': EMAIL_PR_REVIEW_SUBJECT_TEMPLATE,
411 413 'email_pr_comment_subject_template': EMAIL_PR_COMMENT_SUBJECT_TEMPLATE,
412 414 'email_pr_comment_status_change_subject_template': EMAIL_PR_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE,
413 415 'email_pr_comment_file_subject_template': EMAIL_PR_COMMENT_FILE_SUBJECT_TEMPLATE,
414 416 'email_comment_subject_template': EMAIL_COMMENT_SUBJECT_TEMPLATE,
415 417 'email_comment_status_change_subject_template': EMAIL_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE,
416 418 'email_comment_file_subject_template': EMAIL_COMMENT_FILE_SUBJECT_TEMPLATE,
417 419 }
418 420 _kwargs.update(kwargs)
419 421 return _kwargs
420 422
421 423 def whitespace_filter(self, text):
422 424 return text.replace('\n', '').replace('\t', '')
423 425
424 426 def get_renderer(self, type_, request):
425 427 template_name = self.email_types[type_]
426 428 return request.get_partial_renderer(template_name)
427 429
428 430 def render_email(self, type_, **kwargs):
429 431 """
430 432 renders template for email, and returns a tuple of
431 433 (subject, email_headers, email_html_body, email_plaintext_body)
432 434 """
433 435 request = get_current_request()
434 436
435 437 # translator and helpers inject
436 438 _kwargs = self._update_kwargs_for_render(kwargs)
437 439 email_template = self.get_renderer(type_, request=request)
438 440 subject = email_template.render('subject', **_kwargs)
439 441
440 442 try:
441 443 body_plaintext = email_template.render('body_plaintext', **_kwargs)
442 444 except AttributeError:
443 445 # it's not defined in template, ok we can skip it
444 446 body_plaintext = ''
445 447
446 448 # render WHOLE template
447 449 body = email_template.render(None, **_kwargs)
448 450
449 451 try:
450 452 # Inline CSS styles and conversion
451 453 body = self.premailer_instance.transform(body)
452 454 except Exception:
453 455 log.exception('Failed to parse body with premailer')
454 456 pass
455 457
456 458 return subject, body, body_plaintext
@@ -1,1195 +1,1203 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import os
20 20 import re
21 21 import shutil
22 22 import time
23 23 import logging
24 24 import traceback
25 25 import datetime
26 26
27 27 from pyramid.threadlocal import get_current_request
28 28 from sqlalchemy.orm import aliased
29 29 from zope.cachedescriptors.property import Lazy as LazyProperty
30 30
31 31 from rhodecode import events
32 32 from rhodecode.lib.auth import HasUserGroupPermissionAny
33 33 from rhodecode.lib.caching_query import FromCache
34 from rhodecode.lib.exceptions import AttachedForksError, AttachedPullRequestsError
34 from rhodecode.lib.exceptions import AttachedForksError, AttachedPullRequestsError, AttachedArtifactsError
35 35 from rhodecode.lib import hooks_base
36 36 from rhodecode.lib.user_log_filter import user_log_filter
37 37 from rhodecode.lib.utils import make_db_config
38 38 from rhodecode.lib.utils2 import (
39 39 safe_str, remove_prefix, obfuscate_url_pw,
40 40 get_current_rhodecode_user, safe_int, action_logger_generic)
41 41 from rhodecode.lib.vcs.backends import get_backend
42 42 from rhodecode.lib.vcs.nodes import NodeKind
43 43 from rhodecode.model import BaseModel
44 44 from rhodecode.model.db import (
45 45 _hash_key, func, case, joinedload, or_, in_filter_generator,
46 46 Session, Repository, UserRepoToPerm, UserGroupRepoToPerm,
47 47 UserRepoGroupToPerm, UserGroupRepoGroupToPerm, User, Permission,
48 48 Statistics, UserGroup, RepoGroup, RepositoryField, UserLog)
49 49 from rhodecode.model.permission import PermissionModel
50 50 from rhodecode.model.settings import VcsSettingsModel
51 51
52 52 log = logging.getLogger(__name__)
53 53
54 54
55 55 class RepoModel(BaseModel):
56 56
57 57 cls = Repository
58 58
59 59 def _get_user_group(self, users_group):
60 60 return self._get_instance(UserGroup, users_group,
61 61 callback=UserGroup.get_by_group_name)
62 62
63 63 def _get_repo_group(self, repo_group):
64 64 return self._get_instance(RepoGroup, repo_group,
65 65 callback=RepoGroup.get_by_group_name)
66 66
67 67 def _create_default_perms(self, repository, private):
68 68 # create default permission
69 69 default = 'repository.read'
70 70 def_user = User.get_default_user()
71 71 for p in def_user.user_perms:
72 72 if p.permission.permission_name.startswith('repository.'):
73 73 default = p.permission.permission_name
74 74 break
75 75
76 76 default_perm = 'repository.none' if private else default
77 77
78 78 repo_to_perm = UserRepoToPerm()
79 79 repo_to_perm.permission = Permission.get_by_key(default_perm)
80 80
81 81 repo_to_perm.repository = repository
82 82 repo_to_perm.user = def_user
83 83
84 84 return repo_to_perm
85 85
86 86 def get(self, repo_id):
87 87 repo = self.sa.query(Repository) \
88 88 .filter(Repository.repo_id == repo_id)
89 89
90 90 return repo.scalar()
91 91
92 92 def get_repo(self, repository):
93 93 return self._get_repo(repository)
94 94
95 95 def get_by_repo_name(self, repo_name, cache=False):
96 96 repo = self.sa.query(Repository) \
97 97 .filter(Repository.repo_name == repo_name)
98 98
99 99 if cache:
100 100 name_key = _hash_key(repo_name)
101 101 repo = repo.options(
102 102 FromCache("sql_cache_short", f"get_repo_{name_key}"))
103 103 return repo.scalar()
104 104
105 105 def _extract_id_from_repo_name(self, repo_name):
106 106 if repo_name.startswith('/'):
107 107 repo_name = repo_name.lstrip('/')
108 108 by_id_match = re.match(r'^_(\d+)', repo_name)
109 109 if by_id_match:
110 110 return by_id_match.groups()[0]
111 111
112 112 def get_repo_by_id(self, repo_name):
113 113 """
114 114 Extracts repo_name by id from special urls.
115 115 Example url is _11/repo_name
116 116
117 117 :param repo_name:
118 118 :return: repo object if matched else None
119 119 """
120 120 _repo_id = None
121 121 try:
122 122 _repo_id = self._extract_id_from_repo_name(repo_name)
123 123 if _repo_id:
124 124 return self.get(_repo_id)
125 125 except Exception:
126 126 log.exception('Failed to extract repo_name from URL')
127 127 if _repo_id:
128 128 Session().rollback()
129 129
130 130 return None
131 131
132 132 def get_repos_for_root(self, root, traverse=False):
133 133 if traverse:
134 134 like_expression = u'{}%'.format(safe_str(root))
135 135 repos = Repository.query().filter(
136 136 Repository.repo_name.like(like_expression)).all()
137 137 else:
138 138 if root and not isinstance(root, RepoGroup):
139 139 raise ValueError(
140 140 'Root must be an instance '
141 141 'of RepoGroup, got:{} instead'.format(type(root)))
142 142 repos = Repository.query().filter(Repository.group == root).all()
143 143 return repos
144 144
145 145 def get_url(self, repo, request=None, permalink=False):
146 146 if not request:
147 147 request = get_current_request()
148 148
149 149 if not request:
150 150 return
151 151
152 152 if permalink:
153 153 return request.route_url(
154 154 'repo_summary', repo_name='_{}'.format(safe_str(repo.repo_id)))
155 155 else:
156 156 return request.route_url(
157 157 'repo_summary', repo_name=safe_str(repo.repo_name))
158 158
159 159 def get_commit_url(self, repo, commit_id, request=None, permalink=False):
160 160 if not request:
161 161 request = get_current_request()
162 162
163 163 if not request:
164 164 return
165 165
166 166 if permalink:
167 167 return request.route_url(
168 168 'repo_commit', repo_name=safe_str(repo.repo_id),
169 169 commit_id=commit_id)
170 170
171 171 else:
172 172 return request.route_url(
173 173 'repo_commit', repo_name=safe_str(repo.repo_name),
174 174 commit_id=commit_id)
175 175
176 176 def get_repo_log(self, repo, filter_term):
177 177 repo_log = UserLog.query()\
178 178 .filter(or_(UserLog.repository_id == repo.repo_id,
179 179 UserLog.repository_name == repo.repo_name))\
180 180 .options(joinedload(UserLog.user))\
181 181 .options(joinedload(UserLog.repository))\
182 182 .order_by(UserLog.action_date.desc())
183 183
184 184 repo_log = user_log_filter(repo_log, filter_term)
185 185 return repo_log
186 186
187 187 @classmethod
188 188 def update_commit_cache(cls, repositories=None):
189 189 if not repositories:
190 190 repositories = Repository.getAll()
191 191 for repo in repositories:
192 192 repo.update_commit_cache()
193 193
194 194 def get_repos_as_dict(self, repo_list=None, admin=False,
195 195 super_user_actions=False, short_name=None):
196 196
197 197 _render = get_current_request().get_partial_renderer(
198 198 'rhodecode:templates/data_table/_dt_elements.mako')
199 199 c = _render.get_call_context()
200 200 h = _render.get_helpers()
201 201
202 202 def quick_menu(repo_name):
203 203 return _render('quick_menu', repo_name)
204 204
205 205 def repo_lnk(name, rtype, rstate, private, archived, fork_repo_name):
206 206 if short_name is not None:
207 207 short_name_var = short_name
208 208 else:
209 209 short_name_var = not admin
210 210 return _render('repo_name', name, rtype, rstate, private, archived, fork_repo_name,
211 211 short_name=short_name_var, admin=False)
212 212
213 213 def last_change(last_change):
214 214 if admin and isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
215 215 ts = time.time()
216 216 utc_offset = (datetime.datetime.fromtimestamp(ts)
217 217 - datetime.datetime.utcfromtimestamp(ts)).total_seconds()
218 218 last_change = last_change + datetime.timedelta(seconds=utc_offset)
219 219
220 220 return _render("last_change", last_change)
221 221
222 222 def rss_lnk(repo_name):
223 223 return _render("rss", repo_name)
224 224
225 225 def atom_lnk(repo_name):
226 226 return _render("atom", repo_name)
227 227
228 228 def last_rev(repo_name, cs_cache):
229 229 return _render('revision', repo_name, cs_cache.get('revision'),
230 230 cs_cache.get('raw_id'), cs_cache.get('author'),
231 231 cs_cache.get('message'), cs_cache.get('date'))
232 232
233 233 def desc(desc):
234 234 return _render('repo_desc', desc, c.visual.stylify_metatags)
235 235
236 236 def state(repo_state):
237 237 return _render("repo_state", repo_state)
238 238
239 239 def repo_actions(repo_name):
240 240 return _render('repo_actions', repo_name, super_user_actions)
241 241
242 242 def user_profile(username):
243 243 return _render('user_profile', username)
244 244
245 245 repos_data = []
246 246 for repo in repo_list:
247 247 # NOTE(marcink): because we use only raw column we need to load it like that
248 248 changeset_cache = Repository._load_changeset_cache(
249 249 repo.repo_id, repo._changeset_cache)
250 250
251 251 row = {
252 252 "menu": quick_menu(repo.repo_name),
253 253
254 254 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
255 255 repo.private, repo.archived, repo.fork_repo_name),
256 256
257 257 "desc": desc(h.escape(repo.description)),
258 258
259 259 "last_change": last_change(repo.updated_on),
260 260
261 261 "last_changeset": last_rev(repo.repo_name, changeset_cache),
262 262 "last_changeset_raw": changeset_cache.get('revision'),
263 263
264 264 "owner": user_profile(repo.owner_username),
265 265
266 266 "state": state(repo.repo_state),
267 267 "rss": rss_lnk(repo.repo_name),
268 268 "atom": atom_lnk(repo.repo_name),
269 269 }
270 270 if admin:
271 271 row.update({
272 272 "action": repo_actions(repo.repo_name),
273 273 })
274 274 repos_data.append(row)
275 275
276 276 return repos_data
277 277
278 278 def get_repos_data_table(
279 279 self, draw, start, limit,
280 280 search_q, order_by, order_dir,
281 281 auth_user, repo_group_id):
282 282 from rhodecode.model.scm import RepoList
283 283
284 284 _perms = ['repository.read', 'repository.write', 'repository.admin']
285 285
286 286 repos = Repository.query() \
287 287 .filter(Repository.group_id == repo_group_id) \
288 288 .all()
289 289 auth_repo_list = RepoList(
290 290 repos, perm_set=_perms,
291 291 extra_kwargs=dict(user=auth_user))
292 292
293 293 allowed_ids = [-1]
294 294 for repo in auth_repo_list:
295 295 allowed_ids.append(repo.repo_id)
296 296
297 297 repos_data_total_count = Repository.query() \
298 298 .filter(Repository.group_id == repo_group_id) \
299 299 .filter(or_(
300 300 # generate multiple IN to fix limitation problems
301 301 *in_filter_generator(Repository.repo_id, allowed_ids))
302 302 ) \
303 303 .count()
304 304
305 305 RepoFork = aliased(Repository)
306 306 OwnerUser = aliased(User)
307 307 base_q = Session.query(
308 308 Repository.repo_id,
309 309 Repository.repo_name,
310 310 Repository.description,
311 311 Repository.repo_type,
312 312 Repository.repo_state,
313 313 Repository.private,
314 314 Repository.archived,
315 315 Repository.updated_on,
316 316 Repository._changeset_cache,
317 317 RepoFork.repo_name.label('fork_repo_name'),
318 318 OwnerUser.username.label('owner_username'),
319 319 ) \
320 320 .filter(Repository.group_id == repo_group_id) \
321 321 .filter(or_(
322 322 # generate multiple IN to fix limitation problems
323 323 *in_filter_generator(Repository.repo_id, allowed_ids))
324 324 ) \
325 325 .outerjoin(RepoFork, Repository.fork_id == RepoFork.repo_id) \
326 326 .join(OwnerUser, Repository.user_id == OwnerUser.user_id)
327 327
328 328 repos_data_total_filtered_count = base_q.count()
329 329
330 330 sort_defined = False
331 331 if order_by == 'repo_name':
332 332 sort_col = func.lower(Repository.repo_name)
333 333 sort_defined = True
334 334 elif order_by == 'user_username':
335 335 sort_col = User.username
336 336 else:
337 337 sort_col = getattr(Repository, order_by, None)
338 338
339 339 if sort_defined or sort_col:
340 340 if order_dir == 'asc':
341 341 sort_col = sort_col.asc()
342 342 else:
343 343 sort_col = sort_col.desc()
344 344
345 345 base_q = base_q.order_by(sort_col)
346 346 base_q = base_q.offset(start).limit(limit)
347 347
348 348 repos_list = base_q.all()
349 349
350 350 repos_data = RepoModel().get_repos_as_dict(
351 351 repo_list=repos_list, admin=False)
352 352
353 353 data = ({
354 354 'draw': draw,
355 355 'data': repos_data,
356 356 'recordsTotal': repos_data_total_count,
357 357 'recordsFiltered': repos_data_total_filtered_count,
358 358 })
359 359 return data
360 360
361 361 def _get_defaults(self, repo_name):
362 362 """
363 363 Gets information about repository, and returns a dict for
364 364 usage in forms
365 365
366 366 :param repo_name:
367 367 """
368 368
369 369 repo_info = Repository.get_by_repo_name(repo_name)
370 370
371 371 if repo_info is None:
372 372 return None
373 373
374 374 defaults = repo_info.get_dict()
375 375 defaults['repo_name'] = repo_info.just_name
376 376
377 377 groups = repo_info.groups_with_parents
378 378 parent_group = groups[-1] if groups else None
379 379
380 380 # we use -1 as this is how in HTML, we mark an empty group
381 381 defaults['repo_group'] = getattr(parent_group, 'group_id', -1)
382 382
383 383 keys_to_process = (
384 384 {'k': 'repo_type', 'strip': False},
385 385 {'k': 'repo_enable_downloads', 'strip': True},
386 386 {'k': 'repo_description', 'strip': True},
387 387 {'k': 'repo_enable_locking', 'strip': True},
388 388 {'k': 'repo_landing_rev', 'strip': True},
389 389 {'k': 'clone_uri', 'strip': False},
390 390 {'k': 'push_uri', 'strip': False},
391 391 {'k': 'repo_private', 'strip': True},
392 392 {'k': 'repo_enable_statistics', 'strip': True}
393 393 )
394 394
395 395 for item in keys_to_process:
396 396 attr = item['k']
397 397 if item['strip']:
398 398 attr = remove_prefix(item['k'], 'repo_')
399 399
400 400 val = defaults[attr]
401 401 if item['k'] == 'repo_landing_rev':
402 402 val = ':'.join(defaults[attr])
403 403 defaults[item['k']] = val
404 404 if item['k'] == 'clone_uri':
405 405 defaults['clone_uri_hidden'] = repo_info.clone_uri_hidden
406 406 if item['k'] == 'push_uri':
407 407 defaults['push_uri_hidden'] = repo_info.push_uri_hidden
408 408
409 409 # fill owner
410 410 if repo_info.user:
411 411 defaults.update({'user': repo_info.user.username})
412 412 else:
413 413 replacement_user = User.get_first_super_admin().username
414 414 defaults.update({'user': replacement_user})
415 415
416 416 return defaults
417 417
418 418 def update(self, repo, **kwargs):
419 419 try:
420 420 cur_repo = self._get_repo(repo)
421 421 source_repo_name = cur_repo.repo_name
422 422
423 423 affected_user_ids = []
424 424 if 'user' in kwargs:
425 425 old_owner_id = cur_repo.user.user_id
426 426 new_owner = User.get_by_username(kwargs['user'])
427 427 cur_repo.user = new_owner
428 428
429 429 if old_owner_id != new_owner.user_id:
430 430 affected_user_ids = [new_owner.user_id, old_owner_id]
431 431
432 432 if 'repo_group' in kwargs:
433 433 cur_repo.group = RepoGroup.get(kwargs['repo_group'])
434 434 log.debug('Updating repo %s with params:%s', cur_repo, kwargs)
435 435
436 436 update_keys = [
437 437 (1, 'repo_description'),
438 438 (1, 'repo_landing_rev'),
439 439 (1, 'repo_private'),
440 440 (1, 'repo_enable_downloads'),
441 441 (1, 'repo_enable_locking'),
442 442 (1, 'repo_enable_statistics'),
443 443 (0, 'clone_uri'),
444 444 (0, 'push_uri'),
445 445 (0, 'fork_id')
446 446 ]
447 447 for strip, k in update_keys:
448 448 if k in kwargs:
449 449 val = kwargs[k]
450 450 if strip:
451 451 k = remove_prefix(k, 'repo_')
452 452
453 453 setattr(cur_repo, k, val)
454 454
455 455 new_name = cur_repo.get_new_name(kwargs['repo_name'])
456 456 cur_repo.repo_name = new_name
457 457
458 458 # if private flag is set, reset default permission to NONE
459 459 if kwargs.get('repo_private'):
460 460 EMPTY_PERM = 'repository.none'
461 461 RepoModel().grant_user_permission(
462 462 repo=cur_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM
463 463 )
464 464 if kwargs.get('repo_landing_rev'):
465 465 landing_rev_val = kwargs['repo_landing_rev']
466 466 RepoModel().set_landing_rev(cur_repo, landing_rev_val)
467 467
468 468 # handle extra fields
469 469 for field in filter(lambda k: k.startswith(RepositoryField.PREFIX), kwargs):
470 470 k = RepositoryField.un_prefix_key(field)
471 471 ex_field = RepositoryField.get_by_key_name(
472 472 key=k, repo=cur_repo)
473 473 if ex_field:
474 474 ex_field.field_value = kwargs[field]
475 475 self.sa.add(ex_field)
476 476
477 477 self.sa.add(cur_repo)
478 478
479 479 if source_repo_name != new_name:
480 480 # rename repository
481 481 self._rename_filesystem_repo(
482 482 old=source_repo_name, new=new_name)
483 483
484 484 if affected_user_ids:
485 485 PermissionModel().trigger_permission_flush(affected_user_ids)
486 486
487 487 return cur_repo
488 488 except Exception:
489 489 log.error(traceback.format_exc())
490 490 raise
491 491
492 492 def _create_repo(self, repo_name, repo_type, description, owner,
493 493 private=False, clone_uri=None, repo_group=None,
494 494 landing_rev=None, fork_of=None,
495 495 copy_fork_permissions=False, enable_statistics=False,
496 496 enable_locking=False, enable_downloads=False,
497 497 copy_group_permissions=False,
498 498 state=Repository.STATE_PENDING):
499 499 """
500 500 Create repository inside database with PENDING state, this should be
501 501 only executed by create() repo. With exception of importing existing
502 502 repos
503 503 """
504 504 from rhodecode.model.scm import ScmModel
505 505
506 506 owner = self._get_user(owner)
507 507 fork_of = self._get_repo(fork_of)
508 508 repo_group = self._get_repo_group(safe_int(repo_group))
509 509 default_landing_ref, _lbl = ScmModel.backend_landing_ref(repo_type)
510 510 landing_rev = landing_rev or default_landing_ref
511 511
512 512 try:
513 513 repo_name = safe_str(repo_name)
514 514 description = safe_str(description)
515 515 # repo name is just a name of repository
516 516 # while repo_name_full is a full qualified name that is combined
517 517 # with name and path of group
518 518 repo_name_full = repo_name
519 519 repo_name = repo_name.split(Repository.NAME_SEP)[-1]
520 520
521 521 new_repo = Repository()
522 522 new_repo.repo_state = state
523 523 new_repo.enable_statistics = False
524 524 new_repo.repo_name = repo_name_full
525 525 new_repo.repo_type = repo_type
526 526 new_repo.user = owner
527 527 new_repo.group = repo_group
528 528 new_repo.description = description or repo_name
529 529 new_repo.private = private
530 530 new_repo.archived = False
531 531 new_repo.clone_uri = clone_uri
532 532 new_repo.landing_rev = landing_rev
533 533
534 534 new_repo.enable_statistics = enable_statistics
535 535 new_repo.enable_locking = enable_locking
536 536 new_repo.enable_downloads = enable_downloads
537 537
538 538 if repo_group:
539 539 new_repo.enable_locking = repo_group.enable_locking
540 540
541 541 if fork_of:
542 542 parent_repo = fork_of
543 543 new_repo.fork = parent_repo
544 544
545 545 events.trigger(events.RepoPreCreateEvent(new_repo))
546 546
547 547 self.sa.add(new_repo)
548 548
549 549 EMPTY_PERM = 'repository.none'
550 550 if fork_of and copy_fork_permissions:
551 551 repo = fork_of
552 552 user_perms = UserRepoToPerm.query() \
553 553 .filter(UserRepoToPerm.repository == repo).all()
554 554 group_perms = UserGroupRepoToPerm.query() \
555 555 .filter(UserGroupRepoToPerm.repository == repo).all()
556 556
557 557 for perm in user_perms:
558 558 UserRepoToPerm.create(
559 559 perm.user, new_repo, perm.permission)
560 560
561 561 for perm in group_perms:
562 562 UserGroupRepoToPerm.create(
563 563 perm.users_group, new_repo, perm.permission)
564 564 # in case we copy permissions and also set this repo to private
565 565 # override the default user permission to make it a private repo
566 566 if private:
567 567 RepoModel(self.sa).grant_user_permission(
568 568 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
569 569
570 570 elif repo_group and copy_group_permissions:
571 571 user_perms = UserRepoGroupToPerm.query() \
572 572 .filter(UserRepoGroupToPerm.group == repo_group).all()
573 573
574 574 group_perms = UserGroupRepoGroupToPerm.query() \
575 575 .filter(UserGroupRepoGroupToPerm.group == repo_group).all()
576 576
577 577 for perm in user_perms:
578 578 perm_name = perm.permission.permission_name.replace(
579 579 'group.', 'repository.')
580 580 perm_obj = Permission.get_by_key(perm_name)
581 581 UserRepoToPerm.create(perm.user, new_repo, perm_obj)
582 582
583 583 for perm in group_perms:
584 584 perm_name = perm.permission.permission_name.replace(
585 585 'group.', 'repository.')
586 586 perm_obj = Permission.get_by_key(perm_name)
587 587 UserGroupRepoToPerm.create(perm.users_group, new_repo, perm_obj)
588 588
589 589 if private:
590 590 RepoModel(self.sa).grant_user_permission(
591 591 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
592 592
593 593 else:
594 594 perm_obj = self._create_default_perms(new_repo, private)
595 595 self.sa.add(perm_obj)
596 596
597 597 # now automatically start following this repository as owner
598 598 ScmModel(self.sa).toggle_following_repo(new_repo.repo_id, owner.user_id)
599 599
600 600 # we need to flush here, in order to check if database won't
601 601 # throw any exceptions, create filesystem dirs at the very end
602 602 self.sa.flush()
603 603 events.trigger(events.RepoCreateEvent(new_repo, actor=owner))
604 604 return new_repo
605 605
606 606 except Exception:
607 607 log.error(traceback.format_exc())
608 608 raise
609 609
610 610 def create(self, form_data, cur_user):
611 611 """
612 612 Create repository using celery tasks
613 613
614 614 :param form_data:
615 615 :param cur_user:
616 616 """
617 617 from rhodecode.lib.celerylib import tasks, run_task
618 618 return run_task(tasks.create_repo, form_data, cur_user)
619 619
620 620 def update_permissions(self, repo, perm_additions=None, perm_updates=None,
621 621 perm_deletions=None, check_perms=True,
622 622 cur_user=None):
623 623 if not perm_additions:
624 624 perm_additions = []
625 625 if not perm_updates:
626 626 perm_updates = []
627 627 if not perm_deletions:
628 628 perm_deletions = []
629 629
630 630 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
631 631
632 632 changes = {
633 633 'added': [],
634 634 'updated': [],
635 635 'deleted': [],
636 636 'default_user_changed': None
637 637 }
638 638
639 639 repo = self._get_repo(repo)
640 640
641 641 # update permissions
642 642 for member_id, perm, member_type in perm_updates:
643 643 member_id = int(member_id)
644 644 if member_type == 'user':
645 645 member_name = User.get(member_id).username
646 646 if member_name == User.DEFAULT_USER:
647 647 # NOTE(dan): detect if we changed permissions for default user
648 648 perm_obj = self.sa.query(UserRepoToPerm) \
649 649 .filter(UserRepoToPerm.user_id == member_id) \
650 650 .filter(UserRepoToPerm.repository == repo) \
651 651 .scalar()
652 652 if perm_obj and perm_obj.permission.permission_name != perm:
653 653 changes['default_user_changed'] = True
654 654
655 655 # this updates also current one if found
656 656 self.grant_user_permission(
657 657 repo=repo, user=member_id, perm=perm)
658 658 elif member_type == 'user_group':
659 659 # check if we have permissions to alter this usergroup
660 660 member_name = UserGroup.get(member_id).users_group_name
661 661 if not check_perms or HasUserGroupPermissionAny(
662 662 *req_perms)(member_name, user=cur_user):
663 663 self.grant_user_group_permission(
664 664 repo=repo, group_name=member_id, perm=perm)
665 665 else:
666 666 raise ValueError("member_type must be 'user' or 'user_group' "
667 667 "got {} instead".format(member_type))
668 668 changes['updated'].append({'type': member_type, 'id': member_id,
669 669 'name': member_name, 'new_perm': perm})
670 670
671 671 # set new permissions
672 672 for member_id, perm, member_type in perm_additions:
673 673 member_id = int(member_id)
674 674 if member_type == 'user':
675 675 member_name = User.get(member_id).username
676 676 self.grant_user_permission(
677 677 repo=repo, user=member_id, perm=perm)
678 678 elif member_type == 'user_group':
679 679 # check if we have permissions to alter this usergroup
680 680 member_name = UserGroup.get(member_id).users_group_name
681 681 if not check_perms or HasUserGroupPermissionAny(
682 682 *req_perms)(member_name, user=cur_user):
683 683 self.grant_user_group_permission(
684 684 repo=repo, group_name=member_id, perm=perm)
685 685 else:
686 686 raise ValueError("member_type must be 'user' or 'user_group' "
687 687 "got {} instead".format(member_type))
688 688
689 689 changes['added'].append({'type': member_type, 'id': member_id,
690 690 'name': member_name, 'new_perm': perm})
691 691 # delete permissions
692 692 for member_id, perm, member_type in perm_deletions:
693 693 member_id = int(member_id)
694 694 if member_type == 'user':
695 695 member_name = User.get(member_id).username
696 696 self.revoke_user_permission(repo=repo, user=member_id)
697 697 elif member_type == 'user_group':
698 698 # check if we have permissions to alter this usergroup
699 699 member_name = UserGroup.get(member_id).users_group_name
700 700 if not check_perms or HasUserGroupPermissionAny(
701 701 *req_perms)(member_name, user=cur_user):
702 702 self.revoke_user_group_permission(
703 703 repo=repo, group_name=member_id)
704 704 else:
705 705 raise ValueError("member_type must be 'user' or 'user_group' "
706 706 "got {} instead".format(member_type))
707 707
708 708 changes['deleted'].append({'type': member_type, 'id': member_id,
709 709 'name': member_name, 'new_perm': perm})
710 710 return changes
711 711
712 712 def create_fork(self, form_data, cur_user):
713 713 """
714 714 Simple wrapper into executing celery task for fork creation
715 715
716 716 :param form_data:
717 717 :param cur_user:
718 718 """
719 719 from rhodecode.lib.celerylib import tasks, run_task
720 720 return run_task(tasks.create_repo_fork, form_data, cur_user)
721 721
722 722 def archive(self, repo):
723 723 """
724 724 Archive given repository. Set archive flag.
725 725
726 726 :param repo:
727 727 """
728 728 repo = self._get_repo(repo)
729 729 if repo:
730 730
731 731 try:
732 732 repo.archived = True
733 733 self.sa.add(repo)
734 734 self.sa.commit()
735 735 except Exception:
736 736 log.error(traceback.format_exc())
737 737 raise
738 738
739 def delete(self, repo, forks=None, pull_requests=None, fs_remove=True, cur_user=None):
739 def delete(self, repo, forks=None, pull_requests=None, artifacts=None, fs_remove=True, cur_user=None):
740 740 """
741 741 Delete given repository, forks parameter defines what do do with
742 742 attached forks. Throws AttachedForksError if deleted repo has attached
743 743 forks
744 744
745 745 :param repo:
746 746 :param forks: str 'delete' or 'detach'
747 747 :param pull_requests: str 'delete' or None
748 :param artifacts: str 'delete' or None
748 749 :param fs_remove: remove(archive) repo from filesystem
749 750 """
750 751 if not cur_user:
751 752 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
752 753 repo = self._get_repo(repo)
753 754 if repo:
754 755 if forks == 'detach':
755 756 for r in repo.forks:
756 757 r.fork = None
757 758 self.sa.add(r)
758 759 elif forks == 'delete':
759 760 for r in repo.forks:
760 761 self.delete(r, forks='delete')
761 762 elif [f for f in repo.forks]:
762 763 raise AttachedForksError()
763 764
764 765 # check for pull requests
765 766 pr_sources = repo.pull_requests_source
766 767 pr_targets = repo.pull_requests_target
767 768 if pull_requests != 'delete' and (pr_sources or pr_targets):
768 769 raise AttachedPullRequestsError()
769 770
771 artifacts_objs = repo.artifacts
772 if artifacts == 'delete':
773 for a in artifacts_objs:
774 self.sa.delete(a)
775 elif [a for a in artifacts_objs]:
776 raise AttachedArtifactsError()
777
770 778 old_repo_dict = repo.get_dict()
771 779 events.trigger(events.RepoPreDeleteEvent(repo))
772 780 try:
773 781 self.sa.delete(repo)
774 782 if fs_remove:
775 783 self._delete_filesystem_repo(repo)
776 784 else:
777 785 log.debug('skipping removal from filesystem')
778 786 old_repo_dict.update({
779 787 'deleted_by': cur_user,
780 788 'deleted_on': time.time(),
781 789 })
782 790 hooks_base.delete_repository(**old_repo_dict)
783 791 events.trigger(events.RepoDeleteEvent(repo))
784 792 except Exception:
785 793 log.error(traceback.format_exc())
786 794 raise
787 795
788 796 def grant_user_permission(self, repo, user, perm):
789 797 """
790 798 Grant permission for user on given repository, or update existing one
791 799 if found
792 800
793 801 :param repo: Instance of Repository, repository_id, or repository name
794 802 :param user: Instance of User, user_id or username
795 803 :param perm: Instance of Permission, or permission_name
796 804 """
797 805 user = self._get_user(user)
798 806 repo = self._get_repo(repo)
799 807 permission = self._get_perm(perm)
800 808
801 809 # check if we have that permission already
802 810 obj = self.sa.query(UserRepoToPerm) \
803 811 .filter(UserRepoToPerm.user == user) \
804 812 .filter(UserRepoToPerm.repository == repo) \
805 813 .scalar()
806 814 if obj is None:
807 815 # create new !
808 816 obj = UserRepoToPerm()
809 817 obj.repository = repo
810 818 obj.user = user
811 819 obj.permission = permission
812 820 self.sa.add(obj)
813 821 log.debug('Granted perm %s to %s on %s', perm, user, repo)
814 822 action_logger_generic(
815 823 'granted permission: {} to user: {} on repo: {}'.format(
816 824 perm, user, repo), namespace='security.repo')
817 825 return obj
818 826
819 827 def revoke_user_permission(self, repo, user):
820 828 """
821 829 Revoke permission for user on given repository
822 830
823 831 :param repo: Instance of Repository, repository_id, or repository name
824 832 :param user: Instance of User, user_id or username
825 833 """
826 834
827 835 user = self._get_user(user)
828 836 repo = self._get_repo(repo)
829 837
830 838 obj = self.sa.query(UserRepoToPerm) \
831 839 .filter(UserRepoToPerm.repository == repo) \
832 840 .filter(UserRepoToPerm.user == user) \
833 841 .scalar()
834 842 if obj:
835 843 self.sa.delete(obj)
836 844 log.debug('Revoked perm on %s on %s', repo, user)
837 845 action_logger_generic(
838 846 'revoked permission from user: {} on repo: {}'.format(
839 847 user, repo), namespace='security.repo')
840 848
841 849 def grant_user_group_permission(self, repo, group_name, perm):
842 850 """
843 851 Grant permission for user group on given repository, or update
844 852 existing one if found
845 853
846 854 :param repo: Instance of Repository, repository_id, or repository name
847 855 :param group_name: Instance of UserGroup, users_group_id,
848 856 or user group name
849 857 :param perm: Instance of Permission, or permission_name
850 858 """
851 859 repo = self._get_repo(repo)
852 860 group_name = self._get_user_group(group_name)
853 861 permission = self._get_perm(perm)
854 862
855 863 # check if we have that permission already
856 864 obj = self.sa.query(UserGroupRepoToPerm) \
857 865 .filter(UserGroupRepoToPerm.users_group == group_name) \
858 866 .filter(UserGroupRepoToPerm.repository == repo) \
859 867 .scalar()
860 868
861 869 if obj is None:
862 870 # create new
863 871 obj = UserGroupRepoToPerm()
864 872
865 873 obj.repository = repo
866 874 obj.users_group = group_name
867 875 obj.permission = permission
868 876 self.sa.add(obj)
869 877 log.debug('Granted perm %s to %s on %s', perm, group_name, repo)
870 878 action_logger_generic(
871 879 'granted permission: {} to usergroup: {} on repo: {}'.format(
872 880 perm, group_name, repo), namespace='security.repo')
873 881
874 882 return obj
875 883
876 884 def revoke_user_group_permission(self, repo, group_name):
877 885 """
878 886 Revoke permission for user group on given repository
879 887
880 888 :param repo: Instance of Repository, repository_id, or repository name
881 889 :param group_name: Instance of UserGroup, users_group_id,
882 890 or user group name
883 891 """
884 892 repo = self._get_repo(repo)
885 893 group_name = self._get_user_group(group_name)
886 894
887 895 obj = self.sa.query(UserGroupRepoToPerm) \
888 896 .filter(UserGroupRepoToPerm.repository == repo) \
889 897 .filter(UserGroupRepoToPerm.users_group == group_name) \
890 898 .scalar()
891 899 if obj:
892 900 self.sa.delete(obj)
893 901 log.debug('Revoked perm to %s on %s', repo, group_name)
894 902 action_logger_generic(
895 903 'revoked permission from usergroup: {} on repo: {}'.format(
896 904 group_name, repo), namespace='security.repo')
897 905
898 906 def delete_stats(self, repo_name):
899 907 """
900 908 removes stats for given repo
901 909
902 910 :param repo_name:
903 911 """
904 912 repo = self._get_repo(repo_name)
905 913 try:
906 914 obj = self.sa.query(Statistics) \
907 915 .filter(Statistics.repository == repo).scalar()
908 916 if obj:
909 917 self.sa.delete(obj)
910 918 except Exception:
911 919 log.error(traceback.format_exc())
912 920 raise
913 921
914 922 def add_repo_field(self, repo_name, field_key, field_label, field_value='',
915 923 field_type='str', field_desc=''):
916 924
917 925 repo = self._get_repo(repo_name)
918 926
919 927 new_field = RepositoryField()
920 928 new_field.repository = repo
921 929 new_field.field_key = field_key
922 930 new_field.field_type = field_type # python type
923 931 new_field.field_value = field_value
924 932 new_field.field_desc = field_desc
925 933 new_field.field_label = field_label
926 934 self.sa.add(new_field)
927 935 return new_field
928 936
929 937 def delete_repo_field(self, repo_name, field_key):
930 938 repo = self._get_repo(repo_name)
931 939 field = RepositoryField.get_by_key_name(field_key, repo)
932 940 if field:
933 941 self.sa.delete(field)
934 942
935 943 def set_landing_rev(self, repo, landing_rev_name):
936 944 if landing_rev_name.startswith('branch:'):
937 945 landing_rev_name = landing_rev_name.split('branch:')[-1]
938 946 scm_instance = repo.scm_instance()
939 947 if scm_instance:
940 948 return scm_instance._remote.set_head_ref(landing_rev_name)
941 949
942 950 def _create_filesystem_repo(self, repo_name, repo_type, repo_group,
943 951 clone_uri=None, repo_store_location=None,
944 952 use_global_config=False, install_hooks=True):
945 953 """
946 954 makes repository on filesystem. It's group aware means it'll create
947 955 a repository within a group, and alter the paths accordingly of
948 956 group location
949 957
950 958 :param repo_name:
951 959 :param alias:
952 960 :param parent:
953 961 :param clone_uri:
954 962 :param repo_store_location:
955 963 """
956 964 from rhodecode.lib.utils import is_valid_repo, is_valid_repo_group
957 965 from rhodecode.model.scm import ScmModel
958 966
959 967 if Repository.NAME_SEP in repo_name:
960 968 raise ValueError(
961 969 'repo_name must not contain groups got `%s`' % repo_name)
962 970
963 971 if isinstance(repo_group, RepoGroup):
964 972 new_parent_path = os.sep.join(repo_group.full_path_splitted)
965 973 else:
966 974 new_parent_path = repo_group or ''
967 975
968 976 if repo_store_location:
969 977 _paths = [repo_store_location]
970 978 else:
971 979 _paths = [self.repos_path, new_parent_path, repo_name]
972 980 # we need to make it str for mercurial
973 981 repo_path = os.path.join(*map(lambda x: safe_str(x), _paths))
974 982
975 983 # check if this path is not a repository
976 984 if is_valid_repo(repo_path, self.repos_path):
977 985 raise Exception(f'This path {repo_path} is a valid repository')
978 986
979 987 # check if this path is a group
980 988 if is_valid_repo_group(repo_path, self.repos_path):
981 989 raise Exception(f'This path {repo_path} is a valid group')
982 990
983 991 log.info('creating repo %s in %s from url: `%s`',
984 992 repo_name, safe_str(repo_path),
985 993 obfuscate_url_pw(clone_uri))
986 994
987 995 backend = get_backend(repo_type)
988 996
989 997 config_repo = None if use_global_config else repo_name
990 998 if config_repo and new_parent_path:
991 999 config_repo = Repository.NAME_SEP.join(
992 1000 (new_parent_path, config_repo))
993 1001 config = make_db_config(clear_session=False, repo=config_repo)
994 1002 config.set('extensions', 'largefiles', '')
995 1003
996 1004 # patch and reset hooks section of UI config to not run any
997 1005 # hooks on creating remote repo
998 1006 config.clear_section('hooks')
999 1007
1000 1008 # TODO: johbo: Unify this, hardcoded "bare=True" does not look nice
1001 1009 if repo_type == 'git':
1002 1010 repo = backend(
1003 1011 repo_path, config=config, create=True, src_url=clone_uri, bare=True,
1004 1012 with_wire={"cache": False})
1005 1013 else:
1006 1014 repo = backend(
1007 1015 repo_path, config=config, create=True, src_url=clone_uri,
1008 1016 with_wire={"cache": False})
1009 1017
1010 1018 if install_hooks:
1011 1019 repo.install_hooks()
1012 1020
1013 1021 log.debug('Created repo %s with %s backend',
1014 1022 safe_str(repo_name), safe_str(repo_type))
1015 1023 return repo
1016 1024
1017 1025 def _rename_filesystem_repo(self, old, new):
1018 1026 """
1019 1027 renames repository on filesystem
1020 1028
1021 1029 :param old: old name
1022 1030 :param new: new name
1023 1031 """
1024 1032 log.info('renaming repo from %s to %s', old, new)
1025 1033
1026 1034 old_path = os.path.join(self.repos_path, old)
1027 1035 new_path = os.path.join(self.repos_path, new)
1028 1036 if os.path.isdir(new_path):
1029 1037 raise Exception(
1030 1038 'Was trying to rename to already existing dir %s' % new_path
1031 1039 )
1032 1040 shutil.move(old_path, new_path)
1033 1041
1034 1042 def _delete_filesystem_repo(self, repo):
1035 1043 """
1036 1044 removes repo from filesystem, the removal is actually made by
1037 1045 added rm__ prefix into dir, and rename internal .hg/.git dirs so this
1038 1046 repository is no longer valid for rhodecode, can be undeleted later on
1039 1047 by reverting the renames on this repository
1040 1048
1041 1049 :param repo: repo object
1042 1050 """
1043 1051 rm_path = os.path.join(self.repos_path, repo.repo_name)
1044 1052 repo_group = repo.group
1045 1053 log.info("delete_filesystem_repo: removing repository %s", rm_path)
1046 1054 # disable hg/git internal that it doesn't get detected as repo
1047 1055 alias = repo.repo_type
1048 1056
1049 1057 config = make_db_config(clear_session=False)
1050 1058 config.set('extensions', 'largefiles', '')
1051 1059 bare = getattr(repo.scm_instance(config=config), 'bare', False)
1052 1060
1053 1061 # skip this for bare git repos
1054 1062 if not bare:
1055 1063 # disable VCS repo
1056 1064 vcs_path = os.path.join(rm_path, '.%s' % alias)
1057 1065 if os.path.exists(vcs_path):
1058 1066 shutil.move(vcs_path, os.path.join(rm_path, 'rm__.%s' % alias))
1059 1067
1060 1068 _now = datetime.datetime.now()
1061 1069 _ms = str(_now.microsecond).rjust(6, '0')
1062 1070 _d = 'rm__{}__{}'.format(_now.strftime('%Y%m%d_%H%M%S_' + _ms),
1063 1071 repo.just_name)
1064 1072 if repo_group:
1065 1073 # if repository is in group, prefix the removal path with the group
1066 1074 args = repo_group.full_path_splitted + [_d]
1067 1075 _d = os.path.join(*args)
1068 1076
1069 1077 if os.path.isdir(rm_path):
1070 1078 shutil.move(rm_path, os.path.join(self.repos_path, _d))
1071 1079
1072 1080 # finally cleanup diff-cache if it exists
1073 1081 cached_diffs_dir = repo.cached_diffs_dir
1074 1082 if os.path.isdir(cached_diffs_dir):
1075 1083 shutil.rmtree(cached_diffs_dir)
1076 1084
1077 1085
1078 1086 class ReadmeFinder:
1079 1087 """
1080 1088 Utility which knows how to find a readme for a specific commit.
1081 1089
1082 1090 The main idea is that this is a configurable algorithm. When creating an
1083 1091 instance you can define parameters, currently only the `default_renderer`.
1084 1092 Based on this configuration the method :meth:`search` behaves slightly
1085 1093 different.
1086 1094 """
1087 1095
1088 1096 readme_re = re.compile(r'^readme(\.[^\.]+)?$', re.IGNORECASE)
1089 1097 path_re = re.compile(r'^docs?', re.IGNORECASE)
1090 1098
1091 1099 default_priorities = {
1092 1100 None: 0,
1093 1101 '.rst': 1,
1094 1102 '.md': 1,
1095 1103 '.rest': 2,
1096 1104 '.mkdn': 2,
1097 1105 '.text': 2,
1098 1106 '.txt': 3,
1099 1107 '.mdown': 3,
1100 1108 '.markdown': 4,
1101 1109 }
1102 1110
1103 1111 path_priority = {
1104 1112 'doc': 0,
1105 1113 'docs': 1,
1106 1114 }
1107 1115
1108 1116 FALLBACK_PRIORITY = 99
1109 1117
1110 1118 RENDERER_TO_EXTENSION = {
1111 1119 'rst': ['.rst', '.rest'],
1112 1120 'markdown': ['.md', 'mkdn', '.mdown', '.markdown'],
1113 1121 }
1114 1122
1115 1123 def __init__(self, default_renderer=None):
1116 1124 self._default_renderer = default_renderer
1117 1125 self._renderer_extensions = self.RENDERER_TO_EXTENSION.get(
1118 1126 default_renderer, [])
1119 1127
1120 1128 def search(self, commit, path='/'):
1121 1129 """
1122 1130 Find a readme in the given `commit`.
1123 1131 """
1124 1132 # firstly, check the PATH type if it is actually a DIR
1125 1133 if commit.get_node(path).kind != NodeKind.DIR:
1126 1134 return None
1127 1135
1128 1136 nodes = commit.get_nodes(path)
1129 1137 matches = self._match_readmes(nodes)
1130 1138 matches = self._sort_according_to_priority(matches)
1131 1139 if matches:
1132 1140 return matches[0].node
1133 1141
1134 1142 paths = self._match_paths(nodes)
1135 1143 paths = self._sort_paths_according_to_priority(paths)
1136 1144 for path in paths:
1137 1145 match = self.search(commit, path=path)
1138 1146 if match:
1139 1147 return match
1140 1148
1141 1149 return None
1142 1150
1143 1151 def _match_readmes(self, nodes):
1144 1152 for node in nodes:
1145 1153 if not node.is_file():
1146 1154 continue
1147 1155 path = node.path.rsplit('/', 1)[-1]
1148 1156 match = self.readme_re.match(path)
1149 1157 if match:
1150 1158 extension = match.group(1)
1151 1159 yield ReadmeMatch(node, match, self._priority(extension))
1152 1160
1153 1161 def _match_paths(self, nodes):
1154 1162 for node in nodes:
1155 1163 if not node.is_dir():
1156 1164 continue
1157 1165 match = self.path_re.match(node.path)
1158 1166 if match:
1159 1167 yield node.path
1160 1168
1161 1169 def _priority(self, extension):
1162 1170 renderer_priority = (
1163 1171 0 if extension in self._renderer_extensions else 1)
1164 1172 extension_priority = self.default_priorities.get(
1165 1173 extension, self.FALLBACK_PRIORITY)
1166 1174 return (renderer_priority, extension_priority)
1167 1175
1168 1176 def _sort_according_to_priority(self, matches):
1169 1177
1170 1178 def priority_and_path(match):
1171 1179 return (match.priority, match.path)
1172 1180
1173 1181 return sorted(matches, key=priority_and_path)
1174 1182
1175 1183 def _sort_paths_according_to_priority(self, paths):
1176 1184
1177 1185 def priority_and_path(path):
1178 1186 return (self.path_priority.get(path, self.FALLBACK_PRIORITY), path)
1179 1187
1180 1188 return sorted(paths, key=priority_and_path)
1181 1189
1182 1190
1183 1191 class ReadmeMatch:
1184 1192
1185 1193 def __init__(self, node, match, priority):
1186 1194 self.node = node
1187 1195 self._match = match
1188 1196 self.priority = priority
1189 1197
1190 1198 @property
1191 1199 def path(self):
1192 1200 return self.node.path
1193 1201
1194 1202 def __repr__(self):
1195 1203 return f'<ReadmeMatch {self.path} priority={self.priority}'
@@ -1,902 +1,888 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import os
20 20 import re
21 21 import logging
22 22 import time
23 23 import functools
24 24 from collections import namedtuple
25 25
26 26 from pyramid.threadlocal import get_current_request
27 27
28 28 from rhodecode.lib import rc_cache
29 29 from rhodecode.lib.hash_utils import sha1_safe
30 30 from rhodecode.lib.html_filters import sanitize_html
31 31 from rhodecode.lib.utils2 import (
32 32 Optional, AttributeDict, safe_str, remove_prefix, str2bool)
33 33 from rhodecode.lib.vcs.backends import base
34 34 from rhodecode.lib.statsd_client import StatsdClient
35 35 from rhodecode.model import BaseModel
36 36 from rhodecode.model.db import (
37 37 RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi, RhodeCodeSetting)
38 38 from rhodecode.model.meta import Session
39 39
40 40
41 41 log = logging.getLogger(__name__)
42 42
43 43
44 44 UiSetting = namedtuple(
45 45 'UiSetting', ['section', 'key', 'value', 'active'])
46 46
47 47 SOCIAL_PLUGINS_LIST = ['github', 'bitbucket', 'twitter', 'google']
48 48
49 49
50 50 class SettingNotFound(Exception):
51 51 def __init__(self, setting_id):
52 52 msg = f'Setting `{setting_id}` is not found'
53 53 super().__init__(msg)
54 54
55 55
56 56 class SettingsModel(BaseModel):
57 57 BUILTIN_HOOKS = (
58 58 RhodeCodeUi.HOOK_REPO_SIZE, RhodeCodeUi.HOOK_PUSH,
59 59 RhodeCodeUi.HOOK_PRE_PUSH, RhodeCodeUi.HOOK_PRETX_PUSH,
60 60 RhodeCodeUi.HOOK_PULL, RhodeCodeUi.HOOK_PRE_PULL,
61 61 RhodeCodeUi.HOOK_PUSH_KEY,)
62 62 HOOKS_SECTION = 'hooks'
63 63
64 64 def __init__(self, sa=None, repo=None):
65 65 self.repo = repo
66 66 self.UiDbModel = RepoRhodeCodeUi if repo else RhodeCodeUi
67 67 self.SettingsDbModel = (
68 68 RepoRhodeCodeSetting if repo else RhodeCodeSetting)
69 69 super().__init__(sa)
70 70
71 71 def get_keyname(self, key_name, prefix='rhodecode_'):
72 72 return f'{prefix}{key_name}'
73 73
74 74 def get_ui_by_key(self, key):
75 75 q = self.UiDbModel.query()
76 76 q = q.filter(self.UiDbModel.ui_key == key)
77 77 q = self._filter_by_repo(RepoRhodeCodeUi, q)
78 78 return q.scalar()
79 79
80 80 def get_ui_by_section(self, section):
81 81 q = self.UiDbModel.query()
82 82 q = q.filter(self.UiDbModel.ui_section == section)
83 83 q = self._filter_by_repo(RepoRhodeCodeUi, q)
84 84 return q.all()
85 85
86 86 def get_ui_by_section_and_key(self, section, key):
87 87 q = self.UiDbModel.query()
88 88 q = q.filter(self.UiDbModel.ui_section == section)
89 89 q = q.filter(self.UiDbModel.ui_key == key)
90 90 q = self._filter_by_repo(RepoRhodeCodeUi, q)
91 91 return q.scalar()
92 92
93 93 def get_ui(self, section=None, key=None):
94 94 q = self.UiDbModel.query()
95 95 q = self._filter_by_repo(RepoRhodeCodeUi, q)
96 96
97 97 if section:
98 98 q = q.filter(self.UiDbModel.ui_section == section)
99 99 if key:
100 100 q = q.filter(self.UiDbModel.ui_key == key)
101 101
102 102 # TODO: mikhail: add caching
103 103 result = [
104 104 UiSetting(
105 105 section=safe_str(r.ui_section), key=safe_str(r.ui_key),
106 106 value=safe_str(r.ui_value), active=r.ui_active
107 107 )
108 108 for r in q.all()
109 109 ]
110 110 return result
111 111
112 112 def get_builtin_hooks(self):
113 113 q = self.UiDbModel.query()
114 114 q = q.filter(self.UiDbModel.ui_key.in_(self.BUILTIN_HOOKS))
115 115 return self._get_hooks(q)
116 116
117 117 def get_custom_hooks(self):
118 118 q = self.UiDbModel.query()
119 119 q = q.filter(~self.UiDbModel.ui_key.in_(self.BUILTIN_HOOKS))
120 120 return self._get_hooks(q)
121 121
122 122 def create_ui_section_value(self, section, val, key=None, active=True):
123 123 new_ui = self.UiDbModel()
124 124 new_ui.ui_section = section
125 125 new_ui.ui_value = val
126 126 new_ui.ui_active = active
127 127
128 128 repository_id = ''
129 129 if self.repo:
130 130 repo = self._get_repo(self.repo)
131 131 repository_id = repo.repo_id
132 132 new_ui.repository_id = repository_id
133 133
134 134 if not key:
135 135 # keys are unique so they need appended info
136 136 if self.repo:
137 137 key = sha1_safe(f'{section}{val}{repository_id}')
138 138 else:
139 139 key = sha1_safe(f'{section}{val}')
140 140
141 141 new_ui.ui_key = key
142 142
143 143 Session().add(new_ui)
144 144 return new_ui
145 145
146 146 def create_or_update_hook(self, key, value):
147 147 ui = (
148 148 self.get_ui_by_section_and_key(self.HOOKS_SECTION, key) or
149 149 self.UiDbModel())
150 150 ui.ui_section = self.HOOKS_SECTION
151 151 ui.ui_active = True
152 152 ui.ui_key = key
153 153 ui.ui_value = value
154 154
155 155 if self.repo:
156 156 repo = self._get_repo(self.repo)
157 157 repository_id = repo.repo_id
158 158 ui.repository_id = repository_id
159 159
160 160 Session().add(ui)
161 161 return ui
162 162
163 163 def delete_ui(self, id_):
164 164 ui = self.UiDbModel.get(id_)
165 165 if not ui:
166 166 raise SettingNotFound(id_)
167 167 Session().delete(ui)
168 168
169 169 def get_setting_by_name(self, name):
170 170 q = self._get_settings_query()
171 171 q = q.filter(self.SettingsDbModel.app_settings_name == name)
172 172 return q.scalar()
173 173
174 174 def create_or_update_setting(
175 175 self, name, val: Optional | str = Optional(''), type_: Optional | str = Optional('unicode')):
176 176 """
177 177 Creates or updates RhodeCode setting. If updates are triggered, it will
178 178 only update parameters that are explicitly set Optional instance will
179 179 be skipped
180 180
181 181 :param name:
182 182 :param val:
183 183 :param type_:
184 184 :return:
185 185 """
186 186
187 187 res = self.get_setting_by_name(name)
188 188 repo = self._get_repo(self.repo) if self.repo else None
189 189
190 190 if not res:
191 191 val = Optional.extract(val)
192 192 type_ = Optional.extract(type_)
193 193
194 194 args = (
195 195 (repo.repo_id, name, val, type_)
196 196 if repo else (name, val, type_))
197 197 res = self.SettingsDbModel(*args)
198 198
199 199 else:
200 200 if self.repo:
201 201 res.repository_id = repo.repo_id
202 202
203 203 res.app_settings_name = name
204 204 if not isinstance(type_, Optional):
205 205 # update if set
206 206 res.app_settings_type = type_
207 207 if not isinstance(val, Optional):
208 208 # update if set
209 209 res.app_settings_value = val
210 210
211 211 Session().add(res)
212 212 return res
213 213
214 214 def get_cache_region(self):
215 215 repo = self._get_repo(self.repo) if self.repo else None
216 216 cache_key = f"repo.v1.{repo.repo_id}" if repo else "repo.v1.ALL"
217 217 cache_namespace_uid = f'cache_settings.{cache_key}'
218 218 region = rc_cache.get_or_create_region('cache_general', cache_namespace_uid)
219 219 return region, cache_namespace_uid
220 220
221 221 def invalidate_settings_cache(self, hard=False):
222 222 region, namespace_key = self.get_cache_region()
223 223 log.debug('Invalidation cache [%s] region %s for cache_key: %s',
224 224 'invalidate_settings_cache', region, namespace_key)
225 225
226 226 # we use hard cleanup if invalidation is sent
227 227 rc_cache.clear_cache_namespace(region, namespace_key, method=rc_cache.CLEAR_DELETE)
228 228
229 229 def get_cache_call_method(self, cache=True):
230 230 region, cache_key = self.get_cache_region()
231 231
232 232 @region.conditional_cache_on_arguments(condition=cache)
233 233 def _get_all_settings(name, key):
234 234 q = self._get_settings_query()
235 235 if not q:
236 236 raise Exception('Could not get application settings !')
237 237
238 238 settings = {
239 239 self.get_keyname(res.app_settings_name): res.app_settings_value
240 240 for res in q
241 241 }
242 242 return settings
243 243 return _get_all_settings
244 244
245 245 def get_all_settings(self, cache=False, from_request=True):
246 246 # defines if we use GLOBAL, or PER_REPO
247 247 repo = self._get_repo(self.repo) if self.repo else None
248 248
249 249 # initially try the request context; this is the fastest
250 250 # we only fetch global config, NOT for repo-specific
251 251 if from_request and not repo:
252 252 request = get_current_request()
253 253
254 254 if request and hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
255 255 rc_config = request.call_context.rc_config
256 256 if rc_config:
257 257 return rc_config
258 258
259 259 _region, cache_key = self.get_cache_region()
260 260 _get_all_settings = self.get_cache_call_method(cache=cache)
261 261
262 262 start = time.time()
263 263 result = _get_all_settings('rhodecode_settings', cache_key)
264 264 compute_time = time.time() - start
265 265 log.debug('cached method:%s took %.4fs', _get_all_settings.__name__, compute_time)
266 266
267 267 statsd = StatsdClient.statsd
268 268 if statsd:
269 269 elapsed_time_ms = round(1000.0 * compute_time) # use ms only
270 270 statsd.timing("rhodecode_settings_timing.histogram", elapsed_time_ms,
271 271 use_decimals=False)
272 272
273 273 log.debug('Fetching app settings for key: %s took: %.4fs: cache: %s', cache_key, compute_time, cache)
274 274
275 275 return result
276 276
277 277 def get_auth_settings(self):
278 278 q = self._get_settings_query()
279 279 q = q.filter(
280 280 self.SettingsDbModel.app_settings_name.startswith('auth_'))
281 281 rows = q.all()
282 282 auth_settings = {
283 283 row.app_settings_name: row.app_settings_value for row in rows}
284 284 return auth_settings
285 285
286 286 def get_auth_plugins(self):
287 287 auth_plugins = self.get_setting_by_name("auth_plugins")
288 288 return auth_plugins.app_settings_value
289 289
290 290 def get_default_repo_settings(self, strip_prefix=False):
291 291 q = self._get_settings_query()
292 292 q = q.filter(
293 293 self.SettingsDbModel.app_settings_name.startswith('default_'))
294 294 rows = q.all()
295 295
296 296 result = {}
297 297 for row in rows:
298 298 key = row.app_settings_name
299 299 if strip_prefix:
300 300 key = remove_prefix(key, prefix='default_')
301 301 result.update({key: row.app_settings_value})
302 302 return result
303 303
304 304 def get_repo(self):
305 305 repo = self._get_repo(self.repo)
306 306 if not repo:
307 307 raise Exception(
308 308 f'Repository `{self.repo}` cannot be found inside the database')
309 309 return repo
310 310
311 311 def _filter_by_repo(self, model, query):
312 312 if self.repo:
313 313 repo = self.get_repo()
314 314 query = query.filter(model.repository_id == repo.repo_id)
315 315 return query
316 316
317 317 def _get_hooks(self, query):
318 318 query = query.filter(self.UiDbModel.ui_section == self.HOOKS_SECTION)
319 319 query = self._filter_by_repo(RepoRhodeCodeUi, query)
320 320 return query.all()
321 321
322 322 def _get_settings_query(self):
323 323 q = self.SettingsDbModel.query()
324 324 return self._filter_by_repo(RepoRhodeCodeSetting, q)
325 325
326 326 def list_enabled_social_plugins(self, settings):
327 327 enabled = []
328 328 for plug in SOCIAL_PLUGINS_LIST:
329 329 if str2bool(settings.get(f'rhodecode_auth_{plug}_enabled')):
330 330 enabled.append(plug)
331 331 return enabled
332 332
333 333
334 334 def assert_repo_settings(func):
335 335 @functools.wraps(func)
336 336 def _wrapper(self, *args, **kwargs):
337 337 if not self.repo_settings:
338 338 raise Exception('Repository is not specified')
339 339 return func(self, *args, **kwargs)
340 340 return _wrapper
341 341
342 342
343 343 class IssueTrackerSettingsModel(object):
344 344 INHERIT_SETTINGS = 'inherit_issue_tracker_settings'
345 345 SETTINGS_PREFIX = 'issuetracker_'
346 346
347 347 def __init__(self, sa=None, repo=None):
348 348 self.global_settings = SettingsModel(sa=sa)
349 349 self.repo_settings = SettingsModel(sa=sa, repo=repo) if repo else None
350 350
351 351 @property
352 352 def inherit_global_settings(self):
353 353 if not self.repo_settings:
354 354 return True
355 355 setting = self.repo_settings.get_setting_by_name(self.INHERIT_SETTINGS)
356 356 return setting.app_settings_value if setting else True
357 357
358 358 @inherit_global_settings.setter
359 359 def inherit_global_settings(self, value):
360 360 if self.repo_settings:
361 361 settings = self.repo_settings.create_or_update_setting(
362 362 self.INHERIT_SETTINGS, value, type_='bool')
363 363 Session().add(settings)
364 364
365 365 def _get_keyname(self, key, uid, prefix='rhodecode_'):
366 366 return f'{prefix}{self.SETTINGS_PREFIX}{key}_{uid}'
367 367
368 368 def _make_dict_for_settings(self, qs):
369 369 prefix_match = self._get_keyname('pat', '',)
370 370
371 371 issuetracker_entries = {}
372 372 # create keys
373 373 for k, v in qs.items():
374 374 if k.startswith(prefix_match):
375 375 uid = k[len(prefix_match):]
376 376 issuetracker_entries[uid] = None
377 377
378 378 def url_cleaner(input_str):
379 379 input_str = input_str.replace('"', '').replace("'", '')
380 380 input_str = sanitize_html(input_str, strip=True)
381 381 return input_str
382 382
383 383 # populate
384 384 for uid in issuetracker_entries:
385 385 url_data = qs.get(self._get_keyname('url', uid))
386 386
387 387 pat = qs.get(self._get_keyname('pat', uid))
388 388 try:
389 389 pat_compiled = re.compile(r'%s' % pat)
390 390 except re.error:
391 391 pat_compiled = None
392 392
393 393 issuetracker_entries[uid] = AttributeDict({
394 394 'pat': pat,
395 395 'pat_compiled': pat_compiled,
396 396 'url': url_cleaner(
397 397 qs.get(self._get_keyname('url', uid)) or ''),
398 398 'pref': sanitize_html(
399 399 qs.get(self._get_keyname('pref', uid)) or ''),
400 400 'desc': qs.get(
401 401 self._get_keyname('desc', uid)),
402 402 })
403 403
404 404 return issuetracker_entries
405 405
406 406 def get_global_settings(self, cache=False):
407 407 """
408 408 Returns list of global issue tracker settings
409 409 """
410 410 defaults = self.global_settings.get_all_settings(cache=cache)
411 411 settings = self._make_dict_for_settings(defaults)
412 412 return settings
413 413
414 414 def get_repo_settings(self, cache=False):
415 415 """
416 416 Returns list of issue tracker settings per repository
417 417 """
418 418 if not self.repo_settings:
419 419 raise Exception('Repository is not specified')
420 420 all_settings = self.repo_settings.get_all_settings(cache=cache)
421 421 settings = self._make_dict_for_settings(all_settings)
422 422 return settings
423 423
424 424 def get_settings(self, cache=False):
425 425 if self.inherit_global_settings:
426 426 return self.get_global_settings(cache=cache)
427 427 else:
428 428 return self.get_repo_settings(cache=cache)
429 429
430 430 def delete_entries(self, uid):
431 431 if self.repo_settings:
432 432 all_patterns = self.get_repo_settings()
433 433 settings_model = self.repo_settings
434 434 else:
435 435 all_patterns = self.get_global_settings()
436 436 settings_model = self.global_settings
437 437 entries = all_patterns.get(uid, [])
438 438
439 439 for del_key in entries:
440 440 setting_name = self._get_keyname(del_key, uid, prefix='')
441 441 entry = settings_model.get_setting_by_name(setting_name)
442 442 if entry:
443 443 Session().delete(entry)
444 444
445 445 Session().commit()
446 446
447 447 def create_or_update_setting(
448 448 self, name, val=Optional(''), type_=Optional('unicode')):
449 449 if self.repo_settings:
450 450 setting = self.repo_settings.create_or_update_setting(
451 451 name, val, type_)
452 452 else:
453 453 setting = self.global_settings.create_or_update_setting(
454 454 name, val, type_)
455 455 return setting
456 456
457 457
458 458 class VcsSettingsModel(object):
459 459
460 460 INHERIT_SETTINGS = 'inherit_vcs_settings'
461 461 GENERAL_SETTINGS = (
462 462 'use_outdated_comments',
463 463 'pr_merge_enabled',
464 464 'hg_use_rebase_for_merging',
465 465 'hg_close_branch_before_merging',
466 466 'git_use_rebase_for_merging',
467 467 'git_close_branch_before_merging',
468 468 'diff_cache',
469 469 )
470 470
471 471 HOOKS_SETTINGS = (
472 472 ('hooks', 'changegroup.repo_size'),
473 473 ('hooks', 'changegroup.push_logger'),
474 474 ('hooks', 'outgoing.pull_logger'),
475 475 )
476 476 HG_SETTINGS = (
477 477 ('extensions', 'largefiles'),
478 478 ('phases', 'publish'),
479 479 ('extensions', 'evolve'),
480 480 ('extensions', 'topic'),
481 481 ('experimental', 'evolution'),
482 482 ('experimental', 'evolution.exchange'),
483 483 )
484 484 GIT_SETTINGS = (
485 485 ('vcs_git_lfs', 'enabled'),
486 486 )
487 487 GLOBAL_HG_SETTINGS = (
488 488 ('extensions', 'largefiles'),
489 ('largefiles', 'usercache'),
490 489 ('phases', 'publish'),
491 490 ('extensions', 'evolve'),
492 491 ('extensions', 'topic'),
493 492 ('experimental', 'evolution'),
494 493 ('experimental', 'evolution.exchange'),
495 494 )
496 495
497 496 GLOBAL_GIT_SETTINGS = (
498 497 ('vcs_git_lfs', 'enabled'),
499 ('vcs_git_lfs', 'store_location')
500 498 )
501 499
502 500 SVN_BRANCH_SECTION = 'vcs_svn_branch'
503 501 SVN_TAG_SECTION = 'vcs_svn_tag'
504 SSL_SETTING = ('web', 'push_ssl')
505 502 PATH_SETTING = ('paths', '/')
506 503
507 504 def __init__(self, sa=None, repo=None):
508 505 self.global_settings = SettingsModel(sa=sa)
509 506 self.repo_settings = SettingsModel(sa=sa, repo=repo) if repo else None
510 507 self._ui_settings = (
511 508 self.HG_SETTINGS + self.GIT_SETTINGS + self.HOOKS_SETTINGS)
512 509 self._svn_sections = (self.SVN_BRANCH_SECTION, self.SVN_TAG_SECTION)
513 510
514 511 @property
515 512 @assert_repo_settings
516 513 def inherit_global_settings(self):
517 514 setting = self.repo_settings.get_setting_by_name(self.INHERIT_SETTINGS)
518 515 return setting.app_settings_value if setting else True
519 516
520 517 @inherit_global_settings.setter
521 518 @assert_repo_settings
522 519 def inherit_global_settings(self, value):
523 520 self.repo_settings.create_or_update_setting(
524 521 self.INHERIT_SETTINGS, value, type_='bool')
525 522
526 523 def get_keyname(self, key_name, prefix='rhodecode_'):
527 524 return f'{prefix}{key_name}'
528 525
529 526 def get_global_svn_branch_patterns(self):
530 527 return self.global_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
531 528
532 529 @assert_repo_settings
533 530 def get_repo_svn_branch_patterns(self):
534 531 return self.repo_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
535 532
536 533 def get_global_svn_tag_patterns(self):
537 534 return self.global_settings.get_ui_by_section(self.SVN_TAG_SECTION)
538 535
539 536 @assert_repo_settings
540 537 def get_repo_svn_tag_patterns(self):
541 538 return self.repo_settings.get_ui_by_section(self.SVN_TAG_SECTION)
542 539
543 540 def get_global_settings(self):
544 541 return self._collect_all_settings(global_=True)
545 542
546 543 @assert_repo_settings
547 544 def get_repo_settings(self):
548 545 return self._collect_all_settings(global_=False)
549 546
550 547 @assert_repo_settings
551 548 def get_repo_settings_inherited(self):
552 549 global_settings = self.get_global_settings()
553 550 global_settings.update(self.get_repo_settings())
554 551 return global_settings
555 552
556 553 @assert_repo_settings
557 554 def create_or_update_repo_settings(
558 555 self, data, inherit_global_settings=False):
559 556 from rhodecode.model.scm import ScmModel
560 557
561 558 self.inherit_global_settings = inherit_global_settings
562 559
563 560 repo = self.repo_settings.get_repo()
564 561 if not inherit_global_settings:
565 562 if repo.repo_type == 'svn':
566 563 self.create_repo_svn_settings(data)
567 564 else:
568 565 self.create_or_update_repo_hook_settings(data)
569 566 self.create_or_update_repo_pr_settings(data)
570 567
571 568 if repo.repo_type == 'hg':
572 569 self.create_or_update_repo_hg_settings(data)
573 570
574 571 if repo.repo_type == 'git':
575 572 self.create_or_update_repo_git_settings(data)
576 573
577 574 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
578 575
579 576 @assert_repo_settings
580 577 def create_or_update_repo_hook_settings(self, data):
581 578 for section, key in self.HOOKS_SETTINGS:
582 579 data_key = self._get_form_ui_key(section, key)
583 580 if data_key not in data:
584 581 raise ValueError(
585 582 f'The given data does not contain {data_key} key')
586 583
587 584 active = data.get(data_key)
588 585 repo_setting = self.repo_settings.get_ui_by_section_and_key(
589 586 section, key)
590 587 if not repo_setting:
591 588 global_setting = self.global_settings.\
592 589 get_ui_by_section_and_key(section, key)
593 590 self.repo_settings.create_ui_section_value(
594 591 section, global_setting.ui_value, key=key, active=active)
595 592 else:
596 593 repo_setting.ui_active = active
597 594 Session().add(repo_setting)
598 595
599 596 def update_global_hook_settings(self, data):
600 597 for section, key in self.HOOKS_SETTINGS:
601 598 data_key = self._get_form_ui_key(section, key)
602 599 if data_key not in data:
603 600 raise ValueError(
604 601 f'The given data does not contain {data_key} key')
605 602 active = data.get(data_key)
606 603 repo_setting = self.global_settings.get_ui_by_section_and_key(
607 604 section, key)
608 605 repo_setting.ui_active = active
609 606 Session().add(repo_setting)
610 607
611 608 @assert_repo_settings
612 609 def create_or_update_repo_pr_settings(self, data):
613 610 return self._create_or_update_general_settings(
614 611 self.repo_settings, data)
615 612
616 613 def create_or_update_global_pr_settings(self, data):
617 614 return self._create_or_update_general_settings(
618 615 self.global_settings, data)
619 616
620 617 @assert_repo_settings
621 618 def create_repo_svn_settings(self, data):
622 619 return self._create_svn_settings(self.repo_settings, data)
623 620
624 621 def _set_evolution(self, settings, is_enabled):
625 622 if is_enabled:
626 623 # if evolve is active set evolution=all
627 624
628 625 self._create_or_update_ui(
629 626 settings, *('experimental', 'evolution'), value='all',
630 627 active=True)
631 628 self._create_or_update_ui(
632 629 settings, *('experimental', 'evolution.exchange'), value='yes',
633 630 active=True)
634 631 # if evolve is active set topics server support
635 632 self._create_or_update_ui(
636 633 settings, *('extensions', 'topic'), value='',
637 634 active=True)
638 635
639 636 else:
640 637 self._create_or_update_ui(
641 638 settings, *('experimental', 'evolution'), value='',
642 639 active=False)
643 640 self._create_or_update_ui(
644 641 settings, *('experimental', 'evolution.exchange'), value='no',
645 642 active=False)
646 643 self._create_or_update_ui(
647 644 settings, *('extensions', 'topic'), value='',
648 645 active=False)
649 646
650 647 @assert_repo_settings
651 648 def create_or_update_repo_hg_settings(self, data):
652 649 largefiles, phases, evolve = \
653 650 self.HG_SETTINGS[:3]
654 651 largefiles_key, phases_key, evolve_key = \
655 652 self._get_settings_keys(self.HG_SETTINGS[:3], data)
656 653
657 654 self._create_or_update_ui(
658 655 self.repo_settings, *largefiles, value='',
659 656 active=data[largefiles_key])
660 657 self._create_or_update_ui(
661 658 self.repo_settings, *evolve, value='',
662 659 active=data[evolve_key])
663 660 self._set_evolution(self.repo_settings, is_enabled=data[evolve_key])
664 661
665 662 self._create_or_update_ui(
666 663 self.repo_settings, *phases, value=safe_str(data[phases_key]))
667 664
668 665 def create_or_update_global_hg_settings(self, data):
669 opts_len = 4
670 largefiles, largefiles_store, phases, evolve \
666 opts_len = 3
667 largefiles, phases, evolve \
671 668 = self.GLOBAL_HG_SETTINGS[:opts_len]
672 largefiles_key, largefiles_store_key, phases_key, evolve_key \
669 largefiles_key, phases_key, evolve_key \
673 670 = self._get_settings_keys(self.GLOBAL_HG_SETTINGS[:opts_len], data)
674 671
675 672 self._create_or_update_ui(
676 673 self.global_settings, *largefiles, value='',
677 674 active=data[largefiles_key])
678 675 self._create_or_update_ui(
679 self.global_settings, *largefiles_store, value=data[largefiles_store_key])
680 self._create_or_update_ui(
681 676 self.global_settings, *phases, value=safe_str(data[phases_key]))
682 677 self._create_or_update_ui(
683 678 self.global_settings, *evolve, value='',
684 679 active=data[evolve_key])
685 680 self._set_evolution(self.global_settings, is_enabled=data[evolve_key])
686 681
687 682 def create_or_update_repo_git_settings(self, data):
688 683 # NOTE(marcink): # comma makes unpack work properly
689 684 lfs_enabled, \
690 685 = self.GIT_SETTINGS
691 686
692 687 lfs_enabled_key, \
693 688 = self._get_settings_keys(self.GIT_SETTINGS, data)
694 689
695 690 self._create_or_update_ui(
696 691 self.repo_settings, *lfs_enabled, value=data[lfs_enabled_key],
697 692 active=data[lfs_enabled_key])
698 693
699 694 def create_or_update_global_git_settings(self, data):
700 lfs_enabled, lfs_store_location \
701 = self.GLOBAL_GIT_SETTINGS
702 lfs_enabled_key, lfs_store_location_key \
703 = self._get_settings_keys(self.GLOBAL_GIT_SETTINGS, data)
695 lfs_enabled = self.GLOBAL_GIT_SETTINGS[0]
696 lfs_enabled_key = self._get_settings_keys(self.GLOBAL_GIT_SETTINGS, data)[0]
704 697
705 698 self._create_or_update_ui(
706 699 self.global_settings, *lfs_enabled, value=data[lfs_enabled_key],
707 700 active=data[lfs_enabled_key])
708 self._create_or_update_ui(
709 self.global_settings, *lfs_store_location,
710 value=data[lfs_store_location_key])
711 701
712 702 def create_or_update_global_svn_settings(self, data):
713 703 # branch/tags patterns
714 704 self._create_svn_settings(self.global_settings, data)
715 705
716 def update_global_ssl_setting(self, value):
717 self._create_or_update_ui(
718 self.global_settings, *self.SSL_SETTING, value=value)
719
720 706 @assert_repo_settings
721 707 def delete_repo_svn_pattern(self, id_):
722 708 ui = self.repo_settings.UiDbModel.get(id_)
723 709 if ui and ui.repository.repo_name == self.repo_settings.repo:
724 710 # only delete if it's the same repo as initialized settings
725 711 self.repo_settings.delete_ui(id_)
726 712 else:
727 713 # raise error as if we wouldn't find this option
728 714 self.repo_settings.delete_ui(-1)
729 715
730 716 def delete_global_svn_pattern(self, id_):
731 717 self.global_settings.delete_ui(id_)
732 718
733 719 @assert_repo_settings
734 720 def get_repo_ui_settings(self, section=None, key=None):
735 721 global_uis = self.global_settings.get_ui(section, key)
736 722 repo_uis = self.repo_settings.get_ui(section, key)
737 723
738 724 filtered_repo_uis = self._filter_ui_settings(repo_uis)
739 725 filtered_repo_uis_keys = [
740 726 (s.section, s.key) for s in filtered_repo_uis]
741 727
742 728 def _is_global_ui_filtered(ui):
743 729 return (
744 730 (ui.section, ui.key) in filtered_repo_uis_keys
745 731 or ui.section in self._svn_sections)
746 732
747 733 filtered_global_uis = [
748 734 ui for ui in global_uis if not _is_global_ui_filtered(ui)]
749 735
750 736 return filtered_global_uis + filtered_repo_uis
751 737
752 738 def get_global_ui_settings(self, section=None, key=None):
753 739 return self.global_settings.get_ui(section, key)
754 740
755 741 def get_ui_settings_as_config_obj(self, section=None, key=None):
756 742 config = base.Config()
757 743
758 744 ui_settings = self.get_ui_settings(section=section, key=key)
759 745
760 746 for entry in ui_settings:
761 747 config.set(entry.section, entry.key, entry.value)
762 748
763 749 return config
764 750
765 751 def get_ui_settings(self, section=None, key=None):
766 752 if not self.repo_settings or self.inherit_global_settings:
767 753 return self.get_global_ui_settings(section, key)
768 754 else:
769 755 return self.get_repo_ui_settings(section, key)
770 756
771 757 def get_svn_patterns(self, section=None):
772 758 if not self.repo_settings:
773 759 return self.get_global_ui_settings(section)
774 760 else:
775 761 return self.get_repo_ui_settings(section)
776 762
777 763 @assert_repo_settings
778 764 def get_repo_general_settings(self):
779 765 global_settings = self.global_settings.get_all_settings()
780 766 repo_settings = self.repo_settings.get_all_settings()
781 767 filtered_repo_settings = self._filter_general_settings(repo_settings)
782 768 global_settings.update(filtered_repo_settings)
783 769 return global_settings
784 770
785 771 def get_global_general_settings(self):
786 772 return self.global_settings.get_all_settings()
787 773
788 774 def get_general_settings(self):
789 775 if not self.repo_settings or self.inherit_global_settings:
790 776 return self.get_global_general_settings()
791 777 else:
792 778 return self.get_repo_general_settings()
793 779
794 780 def _filter_ui_settings(self, settings):
795 781 filtered_settings = [
796 782 s for s in settings if self._should_keep_setting(s)]
797 783 return filtered_settings
798 784
799 785 def _should_keep_setting(self, setting):
800 786 keep = (
801 787 (setting.section, setting.key) in self._ui_settings or
802 788 setting.section in self._svn_sections)
803 789 return keep
804 790
805 791 def _filter_general_settings(self, settings):
806 792 keys = [self.get_keyname(key) for key in self.GENERAL_SETTINGS]
807 793 return {
808 794 k: settings[k]
809 795 for k in settings if k in keys}
810 796
811 797 def _collect_all_settings(self, global_=False):
812 798 settings = self.global_settings if global_ else self.repo_settings
813 799 result = {}
814 800
815 801 for section, key in self._ui_settings:
816 802 ui = settings.get_ui_by_section_and_key(section, key)
817 803 result_key = self._get_form_ui_key(section, key)
818 804
819 805 if ui:
820 806 if section in ('hooks', 'extensions'):
821 807 result[result_key] = ui.ui_active
822 808 elif result_key in ['vcs_git_lfs_enabled']:
823 809 result[result_key] = ui.ui_active
824 810 else:
825 811 result[result_key] = ui.ui_value
826 812
827 813 for name in self.GENERAL_SETTINGS:
828 814 setting = settings.get_setting_by_name(name)
829 815 if setting:
830 816 result_key = self.get_keyname(name)
831 817 result[result_key] = setting.app_settings_value
832 818
833 819 return result
834 820
835 821 def _get_form_ui_key(self, section, key):
836 822 return '{section}_{key}'.format(
837 823 section=section, key=key.replace('.', '_'))
838 824
839 825 def _create_or_update_ui(
840 826 self, settings, section, key, value=None, active=None):
841 827 ui = settings.get_ui_by_section_and_key(section, key)
842 828 if not ui:
843 829 active = True if active is None else active
844 830 settings.create_ui_section_value(
845 831 section, value, key=key, active=active)
846 832 else:
847 833 if active is not None:
848 834 ui.ui_active = active
849 835 if value is not None:
850 836 ui.ui_value = value
851 837 Session().add(ui)
852 838
853 839 def _create_svn_settings(self, settings, data):
854 840 svn_settings = {
855 841 'new_svn_branch': self.SVN_BRANCH_SECTION,
856 842 'new_svn_tag': self.SVN_TAG_SECTION
857 843 }
858 844 for key in svn_settings:
859 845 if data.get(key):
860 846 settings.create_ui_section_value(svn_settings[key], data[key])
861 847
862 848 def _create_or_update_general_settings(self, settings, data):
863 849 for name in self.GENERAL_SETTINGS:
864 850 data_key = self.get_keyname(name)
865 851 if data_key not in data:
866 852 raise ValueError(
867 853 f'The given data does not contain {data_key} key')
868 854 setting = settings.create_or_update_setting(
869 855 name, data[data_key], 'bool')
870 856 Session().add(setting)
871 857
872 858 def _get_settings_keys(self, settings, data):
873 859 data_keys = [self._get_form_ui_key(*s) for s in settings]
874 860 for data_key in data_keys:
875 861 if data_key not in data:
876 862 raise ValueError(
877 863 f'The given data does not contain {data_key} key')
878 864 return data_keys
879 865
880 866 def create_largeobjects_dirs_if_needed(self, repo_store_path):
881 867 """
882 868 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
883 869 does a repository scan if enabled in the settings.
884 870 """
885 871
886 872 from rhodecode.lib.vcs.backends.hg import largefiles_store
887 873 from rhodecode.lib.vcs.backends.git import lfs_store
888 874
889 875 paths = [
890 876 largefiles_store(repo_store_path),
891 877 lfs_store(repo_store_path)]
892 878
893 879 for path in paths:
894 880 if os.path.isdir(path):
895 881 continue
896 882 if os.path.isfile(path):
897 883 continue
898 884 # not a file nor dir, we try to create it
899 885 try:
900 886 os.makedirs(path)
901 887 except Exception:
902 888 log.warning('Failed to create largefiles dir:%s', path)
@@ -1,1055 +1,1055 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 users model for RhodeCode
21 21 """
22 22
23 23 import logging
24 24 import traceback
25 25 import datetime
26 26 import ipaddress
27 27
28 28 from pyramid.threadlocal import get_current_request
29 29 from sqlalchemy.exc import DatabaseError
30 30
31 31 from rhodecode import events
32 32 from rhodecode.lib.user_log_filter import user_log_filter
33 33 from rhodecode.lib.utils2 import (
34 34 get_current_rhodecode_user, action_logger_generic,
35 35 AttributeDict, str2bool)
36 36 from rhodecode.lib.str_utils import safe_str
37 37 from rhodecode.lib.exceptions import (
38 38 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
39 39 UserOwnsUserGroupsException, NotAllowedToCreateUserError,
40 40 UserOwnsPullRequestsException, UserOwnsArtifactsException, DuplicateUpdateUserError)
41 41 from rhodecode.lib.caching_query import FromCache
42 42 from rhodecode.model import BaseModel
43 43 from rhodecode.model.db import (
44 44 _hash_key, func, true, false, or_, joinedload, User, UserToPerm,
45 45 UserEmailMap, UserIpMap, UserLog)
46 46 from rhodecode.model.meta import Session
47 47 from rhodecode.model.auth_token import AuthTokenModel
48 48 from rhodecode.model.repo_group import RepoGroupModel
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52
53 53 class UserModel(BaseModel):
54 54 cls = User
55 55
56 56 def get(self, user_id, cache=False):
57 57 user = self.sa.query(User)
58 58 if cache:
59 59 user = user.options(
60 60 FromCache("sql_cache_short", f"get_user_{user_id}"))
61 61 return user.get(user_id)
62 62
63 63 def get_user(self, user):
64 64 return self._get_user(user)
65 65
66 66 def _serialize_user(self, user):
67 67 import rhodecode.lib.helpers as h
68 68
69 69 return {
70 70 'id': user.user_id,
71 71 'first_name': user.first_name,
72 72 'last_name': user.last_name,
73 73 'username': user.username,
74 74 'email': user.email,
75 75 'icon_link': h.gravatar_url(user.email, 30),
76 76 'profile_link': h.link_to_user(user),
77 77 'value_display': h.escape(h.person(user)),
78 78 'value': user.username,
79 79 'value_type': 'user',
80 80 'active': user.active,
81 81 }
82 82
83 83 def get_users(self, name_contains=None, limit=20, only_active=True):
84 84
85 85 query = self.sa.query(User)
86 86 if only_active:
87 87 query = query.filter(User.active == true())
88 88
89 89 if name_contains:
90 90 ilike_expression = f'%{safe_str(name_contains)}%'
91 91 query = query.filter(
92 92 or_(
93 93 User.name.ilike(ilike_expression),
94 94 User.lastname.ilike(ilike_expression),
95 95 User.username.ilike(ilike_expression)
96 96 )
97 97 )
98 98 # sort by len to have top most matches first
99 99 query = query.order_by(func.length(User.username))\
100 100 .order_by(User.username)
101 101 query = query.limit(limit)
102 102
103 103 users = query.all()
104 104
105 105 _users = [
106 106 self._serialize_user(user) for user in users
107 107 ]
108 108 return _users
109 109
110 110 def get_by_username(self, username, cache=False, case_insensitive=False):
111 111
112 112 if case_insensitive:
113 113 user = self.sa.query(User).filter(User.username.ilike(username))
114 114 else:
115 115 user = self.sa.query(User)\
116 116 .filter(User.username == username)
117 117
118 118 if cache:
119 119 name_key = _hash_key(username)
120 120 user = user.options(
121 121 FromCache("sql_cache_short", f"get_user_{name_key}"))
122 122 return user.scalar()
123 123
124 124 def get_by_email(self, email, cache=False, case_insensitive=False):
125 125 return User.get_by_email(email, case_insensitive, cache)
126 126
127 127 def get_by_auth_token(self, auth_token, cache=False):
128 128 return User.get_by_auth_token(auth_token, cache)
129 129
130 130 def get_active_user_count(self, cache=False):
131 131 qry = User.query().filter(
132 132 User.active == true()).filter(
133 133 User.username != User.DEFAULT_USER)
134 134 if cache:
135 135 qry = qry.options(
136 136 FromCache("sql_cache_short", "get_active_users"))
137 137 return qry.count()
138 138
139 139 def create(self, form_data, cur_user=None):
140 140 if not cur_user:
141 141 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
142 142
143 143 user_data = {
144 144 'username': form_data['username'],
145 145 'password': form_data['password'],
146 146 'email': form_data['email'],
147 147 'firstname': form_data['firstname'],
148 148 'lastname': form_data['lastname'],
149 149 'active': form_data['active'],
150 150 'extern_type': form_data['extern_type'],
151 151 'extern_name': form_data['extern_name'],
152 152 'admin': False,
153 153 'cur_user': cur_user
154 154 }
155 155
156 156 if 'create_repo_group' in form_data:
157 157 user_data['create_repo_group'] = str2bool(
158 158 form_data.get('create_repo_group'))
159 159
160 160 try:
161 161 if form_data.get('password_change'):
162 162 user_data['force_password_change'] = True
163 163 return UserModel().create_or_update(**user_data)
164 164 except Exception:
165 165 log.error(traceback.format_exc())
166 166 raise
167 167
168 168 def update_user(self, user, skip_attrs=None, **kwargs):
169 169 from rhodecode.lib.auth import get_crypt_password
170 170
171 171 user = self._get_user(user)
172 172 if user.username == User.DEFAULT_USER:
173 173 raise DefaultUserException(
174 174 "You can't edit this user (`%(username)s`) since it's "
175 175 "crucial for entire application" % {
176 176 'username': user.username})
177 177
178 178 # first store only defaults
179 179 user_attrs = {
180 180 'updating_user_id': user.user_id,
181 181 'username': user.username,
182 182 'password': user.password,
183 183 'email': user.email,
184 184 'firstname': user.name,
185 185 'lastname': user.lastname,
186 186 'description': user.description,
187 187 'active': user.active,
188 188 'admin': user.admin,
189 189 'extern_name': user.extern_name,
190 190 'extern_type': user.extern_type,
191 191 'language': user.user_data.get('language')
192 192 }
193 193
194 194 # in case there's new_password, that comes from form, use it to
195 195 # store password
196 196 if kwargs.get('new_password'):
197 197 kwargs['password'] = kwargs['new_password']
198 198
199 199 # cleanups, my_account password change form
200 200 kwargs.pop('current_password', None)
201 201 kwargs.pop('new_password', None)
202 202
203 203 # cleanups, user edit password change form
204 204 kwargs.pop('password_confirmation', None)
205 205 kwargs.pop('password_change', None)
206 206
207 207 # create repo group on user creation
208 208 kwargs.pop('create_repo_group', None)
209 209
210 210 # legacy forms send name, which is the firstname
211 211 firstname = kwargs.pop('name', None)
212 212 if firstname:
213 213 kwargs['firstname'] = firstname
214 214
215 215 for k, v in kwargs.items():
216 216 # skip if we don't want to update this
217 217 if skip_attrs and k in skip_attrs:
218 218 continue
219 219
220 220 user_attrs[k] = v
221 221
222 222 try:
223 223 return self.create_or_update(**user_attrs)
224 224 except Exception:
225 225 log.error(traceback.format_exc())
226 226 raise
227 227
228 228 def create_or_update(
229 229 self, username, password, email, firstname='', lastname='',
230 230 active=True, admin=False, extern_type=None, extern_name=None,
231 231 cur_user=None, plugin=None, force_password_change=False,
232 232 allow_to_create_user=True, create_repo_group=None,
233 233 updating_user_id=None, language=None, description='',
234 234 strict_creation_check=True):
235 235 """
236 236 Creates a new instance if not found, or updates current one
237 237
238 238 :param username:
239 239 :param password:
240 240 :param email:
241 241 :param firstname:
242 242 :param lastname:
243 243 :param active:
244 244 :param admin:
245 245 :param extern_type:
246 246 :param extern_name:
247 247 :param cur_user:
248 248 :param plugin: optional plugin this method was called from
249 249 :param force_password_change: toggles new or existing user flag
250 250 for password change
251 251 :param allow_to_create_user: Defines if the method can actually create
252 252 new users
253 253 :param create_repo_group: Defines if the method should also
254 254 create an repo group with user name, and owner
255 255 :param updating_user_id: if we set it up this is the user we want to
256 256 update this allows to editing username.
257 257 :param language: language of user from interface.
258 258 :param description: user description
259 259 :param strict_creation_check: checks for allowed creation license wise etc.
260 260
261 261 :returns: new User object with injected `is_new_user` attribute.
262 262 """
263 263
264 264 if not cur_user:
265 265 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
266 266
267 267 from rhodecode.lib.auth import (
268 268 get_crypt_password, check_password)
269 269 from rhodecode.lib import hooks_base
270 270
271 271 def _password_change(new_user, password):
272 272 old_password = new_user.password or ''
273 273 # empty password
274 274 if not old_password:
275 275 return False
276 276
277 277 # password check is only needed for RhodeCode internal auth calls
278 278 # in case it's a plugin we don't care
279 279 if not plugin:
280 280
281 281 # first check if we gave crypted password back, and if it
282 282 # matches it's not password change
283 283 if new_user.password == password:
284 284 return False
285 285
286 286 password_match = check_password(password, old_password)
287 287 if not password_match:
288 288 return True
289 289
290 290 return False
291 291
292 292 # read settings on default personal repo group creation
293 293 if create_repo_group is None:
294 294 default_create_repo_group = RepoGroupModel()\
295 295 .get_default_create_personal_repo_group()
296 296 create_repo_group = default_create_repo_group
297 297
298 298 user_data = {
299 299 'username': username,
300 300 'password': password,
301 301 'email': email,
302 302 'firstname': firstname,
303 303 'lastname': lastname,
304 304 'active': active,
305 305 'admin': admin
306 306 }
307 307
308 308 if updating_user_id:
309 309 log.debug('Checking for existing account in RhodeCode '
310 310 'database with user_id `%s` ', updating_user_id)
311 311 user = User.get(updating_user_id)
312 312 # now also validate if USERNAME belongs to potentially other user
313 313 maybe_other_user = User.get_by_username(username, case_insensitive=True)
314 314 if maybe_other_user and maybe_other_user.user_id != updating_user_id:
315 315 raise DuplicateUpdateUserError(f'different user exists with the {username} username')
316 316 else:
317 317 log.debug('Checking for existing account in RhodeCode '
318 318 'database with username `%s` ', username)
319 319 user = User.get_by_username(username, case_insensitive=True)
320 320
321 321 if user is None:
322 322 # we check internal flag if this method is actually allowed to
323 323 # create new user
324 324 if not allow_to_create_user:
325 325 msg = ('Method wants to create new user, but it is not '
326 326 'allowed to do so')
327 327 log.warning(msg)
328 328 raise NotAllowedToCreateUserError(msg)
329 329
330 330 log.debug('Creating new user %s', username)
331 331
332 332 # only if we create user that is active
333 333 new_active_user = active
334 334 if new_active_user and strict_creation_check:
335 335 # raises UserCreationError if it's not allowed for any reason to
336 336 # create new active user, this also executes pre-create hooks
337 337 hooks_base.check_allowed_create_user(user_data, cur_user, strict_check=True)
338 338 events.trigger(events.UserPreCreate(user_data))
339 339 new_user = User()
340 340 edit = False
341 341 else:
342 342 log.debug('updating user `%s`', username)
343 343 events.trigger(events.UserPreUpdate(user, user_data))
344 344 new_user = user
345 345 edit = True
346 346
347 347 # we're not allowed to edit default user
348 348 if user.username == User.DEFAULT_USER:
349 349 raise DefaultUserException(
350 350 "You can't edit this user (`%(username)s`) since it's "
351 351 "crucial for entire application"
352 352 % {'username': user.username})
353 353
354 354 # inject special attribute that will tell us if User is new or old
355 355 new_user.is_new_user = not edit
356 356 # for users that didn's specify auth type, we use RhodeCode built in
357 357 from rhodecode.authentication.plugins import auth_rhodecode
358 358 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.uid
359 359 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.uid
360 360
361 361 try:
362 362 new_user.username = username
363 363 new_user.admin = admin
364 364 new_user.email = email
365 365 new_user.active = active
366 366 new_user.extern_name = safe_str(extern_name)
367 367 new_user.extern_type = safe_str(extern_type)
368 368 new_user.name = firstname
369 369 new_user.lastname = lastname
370 370 new_user.description = description
371 371
372 372 # set password only if creating an user or password is changed
373 373 if not edit or _password_change(new_user, password):
374 374 reason = 'new password' if edit else 'new user'
375 375 log.debug('Updating password reason=>%s', reason)
376 376 new_user.password = get_crypt_password(password) if password else None
377 377
378 378 if force_password_change:
379 379 new_user.update_userdata(force_password_change=True)
380 380 if language:
381 381 new_user.update_userdata(language=language)
382 382 new_user.update_userdata(notification_status=True)
383 383
384 384 self.sa.add(new_user)
385 385
386 386 if not edit and create_repo_group:
387 387 RepoGroupModel().create_personal_repo_group(
388 388 new_user, commit_early=False)
389 389
390 390 if not edit:
391 391 # add the RSS token
392 392 self.add_auth_token(
393 393 user=username, lifetime_minutes=-1,
394 394 role=self.auth_token_role.ROLE_FEED,
395 395 description='Generated feed token')
396 396
397 397 kwargs = new_user.get_dict()
398 398 # backward compat, require api_keys present
399 399 kwargs['api_keys'] = kwargs['auth_tokens']
400 400 hooks_base.create_user(created_by=cur_user, **kwargs)
401 401 events.trigger(events.UserPostCreate(user_data))
402 402 return new_user
403 403 except (DatabaseError,):
404 404 log.error(traceback.format_exc())
405 405 raise
406 406
407 407 def create_registration(self, form_data,
408 408 extern_name='rhodecode', extern_type='rhodecode'):
409 409 from rhodecode.model.notification import NotificationModel
410 410 from rhodecode.model.notification import EmailNotificationModel
411 411
412 412 try:
413 413 form_data['admin'] = False
414 414 form_data['extern_name'] = extern_name
415 415 form_data['extern_type'] = extern_type
416 416 new_user = self.create(form_data)
417 417
418 418 self.sa.add(new_user)
419 419 self.sa.flush()
420 420
421 421 user_data = new_user.get_dict()
422 422 user_data.update({
423 423 'first_name': user_data.get('firstname'),
424 424 'last_name': user_data.get('lastname'),
425 425 })
426 426 kwargs = {
427 427 # use SQLALCHEMY safe dump of user data
428 428 'user': AttributeDict(user_data),
429 429 'date': datetime.datetime.now()
430 430 }
431 431 notification_type = EmailNotificationModel.TYPE_REGISTRATION
432 432
433 433 # create notification objects, and emails
434 434 NotificationModel().create(
435 435 created_by=new_user,
436 436 notification_subject='', # Filled in based on the notification_type
437 437 notification_body='', # Filled in based on the notification_type
438 438 notification_type=notification_type,
439 439 recipients=None, # all admins
440 440 email_kwargs=kwargs,
441 441 )
442 442
443 443 return new_user
444 444 except Exception:
445 445 log.error(traceback.format_exc())
446 446 raise
447 447
448 448 def _handle_user_repos(self, username, repositories, handle_user,
449 449 handle_mode=None):
450 450
451 451 left_overs = True
452 452
453 453 from rhodecode.model.repo import RepoModel
454 454
455 455 if handle_mode == 'detach':
456 456 for obj in repositories:
457 457 obj.user = handle_user
458 458 # set description we know why we super admin now owns
459 459 # additional repositories that were orphaned !
460 460 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
461 461 self.sa.add(obj)
462 462 left_overs = False
463 463 elif handle_mode == 'delete':
464 464 for obj in repositories:
465 465 RepoModel().delete(obj, forks='detach')
466 466 left_overs = False
467 467
468 468 # if nothing is done we have left overs left
469 469 return left_overs
470 470
471 471 def _handle_user_repo_groups(self, username, repository_groups, handle_user,
472 472 handle_mode=None):
473 473
474 474 left_overs = True
475 475
476 476 from rhodecode.model.repo_group import RepoGroupModel
477 477
478 478 if handle_mode == 'detach':
479 479 for r in repository_groups:
480 480 r.user = handle_user
481 481 # set description we know why we super admin now owns
482 482 # additional repositories that were orphaned !
483 483 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
484 484 r.personal = False
485 485 self.sa.add(r)
486 486 left_overs = False
487 487 elif handle_mode == 'delete':
488 488 for r in repository_groups:
489 489 RepoGroupModel().delete(r)
490 490 left_overs = False
491 491
492 492 # if nothing is done we have left overs left
493 493 return left_overs
494 494
495 495 def _handle_user_user_groups(self, username, user_groups, handle_user,
496 496 handle_mode=None):
497 497
498 498 left_overs = True
499 499
500 500 from rhodecode.model.user_group import UserGroupModel
501 501
502 502 if handle_mode == 'detach':
503 503 for r in user_groups:
504 504 for user_user_group_to_perm in r.user_user_group_to_perm:
505 505 if user_user_group_to_perm.user.username == username:
506 506 user_user_group_to_perm.user = handle_user
507 507 r.user = handle_user
508 508 # set description we know why we super admin now owns
509 509 # additional repositories that were orphaned !
510 510 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
511 511 self.sa.add(r)
512 512 left_overs = False
513 513 elif handle_mode == 'delete':
514 514 for r in user_groups:
515 515 UserGroupModel().delete(r)
516 516 left_overs = False
517 517
518 518 # if nothing is done we have left overs left
519 519 return left_overs
520 520
521 521 def _handle_user_pull_requests(self, username, pull_requests, handle_user,
522 522 handle_mode=None):
523 523 left_overs = True
524 524
525 525 from rhodecode.model.pull_request import PullRequestModel
526 526
527 527 if handle_mode == 'detach':
528 528 for pr in pull_requests:
529 529 pr.user_id = handle_user.user_id
530 530 # set description we know why we super admin now owns
531 531 # additional repositories that were orphaned !
532 532 pr.description += ' \n::detached pull requests from deleted user: %s' % (username,)
533 533 self.sa.add(pr)
534 534 left_overs = False
535 535 elif handle_mode == 'delete':
536 536 for pr in pull_requests:
537 537 PullRequestModel().delete(pr)
538 538
539 539 left_overs = False
540 540
541 541 # if nothing is done we have leftovers left
542 542 return left_overs
543 543
544 544 def _handle_user_artifacts(self, username, artifacts, handle_user,
545 545 handle_mode=None):
546 546
547 547 left_overs = True
548 548
549 549 if handle_mode == 'detach':
550 550 for a in artifacts:
551 551 a.upload_user = handle_user
552 552 # set description we know why we super admin now owns
553 553 # additional artifacts that were orphaned !
554 554 a.file_description += ' \n::detached artifact from deleted user: %s' % (username,)
555 555 self.sa.add(a)
556 556 left_overs = False
557 557 elif handle_mode == 'delete':
558 558 from rhodecode.apps.file_store import utils as store_utils
559 559 request = get_current_request()
560 storage = store_utils.get_file_storage(request.registry.settings)
560 f_store = store_utils.get_filestore_backend(request.registry.settings)
561 561 for a in artifacts:
562 562 file_uid = a.file_uid
563 storage.delete(file_uid)
563 f_store.delete(file_uid)
564 564 self.sa.delete(a)
565 565
566 566 left_overs = False
567 567
568 568 # if nothing is done we have left overs left
569 569 return left_overs
570 570
571 571 def delete(self, user, cur_user=None, handle_repos=None,
572 572 handle_repo_groups=None, handle_user_groups=None,
573 573 handle_pull_requests=None, handle_artifacts=None, handle_new_owner=None):
574 574 from rhodecode.lib import hooks_base
575 575
576 576 if not cur_user:
577 577 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
578 578
579 579 user = self._get_user(user)
580 580
581 581 try:
582 582 if user.username == User.DEFAULT_USER:
583 583 raise DefaultUserException(
584 584 "You can't remove this user since it's"
585 585 " crucial for entire application")
586 586 handle_user = handle_new_owner or self.cls.get_first_super_admin()
587 587 log.debug('New detached objects owner %s', handle_user)
588 588
589 589 left_overs = self._handle_user_repos(
590 590 user.username, user.repositories, handle_user, handle_repos)
591 591 if left_overs and user.repositories:
592 592 repos = [x.repo_name for x in user.repositories]
593 593 raise UserOwnsReposException(
594 594 'user "%(username)s" still owns %(len_repos)s repositories and cannot be '
595 595 'removed. Switch owners or remove those repositories:%(list_repos)s'
596 596 % {'username': user.username, 'len_repos': len(repos),
597 597 'list_repos': ', '.join(repos)})
598 598
599 599 left_overs = self._handle_user_repo_groups(
600 600 user.username, user.repository_groups, handle_user, handle_repo_groups)
601 601 if left_overs and user.repository_groups:
602 602 repo_groups = [x.group_name for x in user.repository_groups]
603 603 raise UserOwnsRepoGroupsException(
604 604 'user "%(username)s" still owns %(len_repo_groups)s repository groups and cannot be '
605 605 'removed. Switch owners or remove those repository groups:%(list_repo_groups)s'
606 606 % {'username': user.username, 'len_repo_groups': len(repo_groups),
607 607 'list_repo_groups': ', '.join(repo_groups)})
608 608
609 609 left_overs = self._handle_user_user_groups(
610 610 user.username, user.user_groups, handle_user, handle_user_groups)
611 611 if left_overs and user.user_groups:
612 612 user_groups = [x.users_group_name for x in user.user_groups]
613 613 raise UserOwnsUserGroupsException(
614 614 'user "%s" still owns %s user groups and cannot be '
615 615 'removed. Switch owners or remove those user groups:%s'
616 616 % (user.username, len(user_groups), ', '.join(user_groups)))
617 617
618 618 left_overs = self._handle_user_pull_requests(
619 619 user.username, user.user_pull_requests, handle_user, handle_pull_requests)
620 620 if left_overs and user.user_pull_requests:
621 621 pull_requests = [f'!{x.pull_request_id}' for x in user.user_pull_requests]
622 622 raise UserOwnsPullRequestsException(
623 623 'user "%s" still owns %s pull requests and cannot be '
624 624 'removed. Switch owners or remove those pull requests:%s'
625 625 % (user.username, len(pull_requests), ', '.join(pull_requests)))
626 626
627 627 left_overs = self._handle_user_artifacts(
628 628 user.username, user.artifacts, handle_user, handle_artifacts)
629 629 if left_overs and user.artifacts:
630 630 artifacts = [x.file_uid for x in user.artifacts]
631 631 raise UserOwnsArtifactsException(
632 632 'user "%s" still owns %s artifacts and cannot be '
633 633 'removed. Switch owners or remove those artifacts:%s'
634 634 % (user.username, len(artifacts), ', '.join(artifacts)))
635 635
636 636 user_data = user.get_dict() # fetch user data before expire
637 637
638 638 # we might change the user data with detach/delete, make sure
639 639 # the object is marked as expired before actually deleting !
640 640 self.sa.expire(user)
641 641 self.sa.delete(user)
642 642
643 643 hooks_base.delete_user(deleted_by=cur_user, **user_data)
644 644 except Exception:
645 645 log.error(traceback.format_exc())
646 646 raise
647 647
648 648 def reset_password_link(self, data, pwd_reset_url):
649 649 from rhodecode.lib.celerylib import tasks, run_task
650 650 from rhodecode.model.notification import EmailNotificationModel
651 651 user_email = data['email']
652 652 try:
653 653 user = User.get_by_email(user_email)
654 654 if user:
655 655 log.debug('password reset user found %s', user)
656 656
657 657 email_kwargs = {
658 658 'password_reset_url': pwd_reset_url,
659 659 'user': user,
660 660 'email': user_email,
661 661 'date': datetime.datetime.now(),
662 662 'first_admin_email': User.get_first_super_admin().email
663 663 }
664 664
665 665 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
666 666 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
667 667
668 668 recipients = [user_email]
669 669
670 670 action_logger_generic(
671 671 'sending password reset email to user: {}'.format(
672 672 user), namespace='security.password_reset')
673 673
674 674 run_task(tasks.send_email, recipients, subject,
675 675 email_body_plaintext, email_body)
676 676
677 677 else:
678 678 log.debug("password reset email %s not found", user_email)
679 679 except Exception:
680 680 log.error(traceback.format_exc())
681 681 return False
682 682
683 683 return True
684 684
685 685 def reset_password(self, data):
686 686 from rhodecode.lib.celerylib import tasks, run_task
687 687 from rhodecode.model.notification import EmailNotificationModel
688 688 from rhodecode.lib import auth
689 689 user_email = data['email']
690 690 pre_db = True
691 691 try:
692 692 user = User.get_by_email(user_email)
693 693 new_passwd = auth.PasswordGenerator().gen_password(
694 694 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
695 695 if user:
696 696 user.password = auth.get_crypt_password(new_passwd)
697 697 # also force this user to reset his password !
698 698 user.update_userdata(force_password_change=True)
699 699
700 700 Session().add(user)
701 701
702 702 # now delete the token in question
703 703 UserApiKeys = AuthTokenModel.cls
704 704 UserApiKeys().query().filter(
705 705 UserApiKeys.api_key == data['token']).delete()
706 706
707 707 Session().commit()
708 708 log.info('successfully reset password for `%s`', user_email)
709 709
710 710 if new_passwd is None:
711 711 raise Exception('unable to generate new password')
712 712
713 713 pre_db = False
714 714
715 715 email_kwargs = {
716 716 'new_password': new_passwd,
717 717 'user': user,
718 718 'email': user_email,
719 719 'date': datetime.datetime.now(),
720 720 'first_admin_email': User.get_first_super_admin().email
721 721 }
722 722
723 723 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
724 724 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
725 725 **email_kwargs)
726 726
727 727 recipients = [user_email]
728 728
729 729 action_logger_generic(
730 730 'sent new password to user: {} with email: {}'.format(
731 731 user, user_email), namespace='security.password_reset')
732 732
733 733 run_task(tasks.send_email, recipients, subject,
734 734 email_body_plaintext, email_body)
735 735
736 736 except Exception:
737 737 log.error('Failed to update user password')
738 738 log.error(traceback.format_exc())
739 739 if pre_db:
740 740 # we rollback only if local db stuff fails. If it goes into
741 741 # run_task, we're pass rollback state this wouldn't work then
742 742 Session().rollback()
743 743
744 744 return True
745 745
746 746 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
747 747 """
748 748 Fetches auth_user by user_id,or api_key if present.
749 749 Fills auth_user attributes with those taken from database.
750 750 Additionally set's is_authenitated if lookup fails
751 751 present in database
752 752
753 753 :param auth_user: instance of user to set attributes
754 754 :param user_id: user id to fetch by
755 755 :param api_key: api key to fetch by
756 756 :param username: username to fetch by
757 757 """
758 758 def token_obfuscate(token):
759 759 if token:
760 760 return token[:4] + "****"
761 761
762 762 if user_id is None and api_key is None and username is None:
763 763 raise Exception('You need to pass user_id, api_key or username')
764 764
765 765 log.debug(
766 766 'AuthUser: fill data execution based on: '
767 767 'user_id:%s api_key:%s username:%s', user_id, api_key, username)
768 768 try:
769 769 found_with = ''
770 770 dbuser = None
771 771 if user_id:
772 772 dbuser = self.get(user_id)
773 773 found_with = 'user_id'
774 774 elif api_key:
775 775 dbuser = self.get_by_auth_token(api_key)
776 776 found_with = 'auth_token'
777 777 elif username:
778 778 dbuser = self.get_by_username(username)
779 779 found_with = 'username'
780 780
781 781 if not dbuser:
782 782 log.warning(
783 783 'Unable to lookup user by id:%s api_key:%s username:%s, found with: %s',
784 784 user_id, token_obfuscate(api_key), username, found_with)
785 785 return False
786 786 if not dbuser.active:
787 787 log.debug('User `%s:%s` is inactive, skipping fill data',
788 788 username, user_id)
789 789 return False
790 790
791 791 log.debug('AuthUser: filling found user:%s data, found with: %s', dbuser, found_with)
792 792
793 793 attrs = {
794 794 'user_id': dbuser.user_id,
795 795 'username': dbuser.username,
796 796 'name': dbuser.name,
797 797 'first_name': dbuser.first_name,
798 798 'firstname': dbuser.firstname,
799 799 'last_name': dbuser.last_name,
800 800 'lastname': dbuser.lastname,
801 801 'admin': dbuser.admin,
802 802 'active': dbuser.active,
803 803
804 804 'email': dbuser.email,
805 805 'emails': dbuser.emails_cached(),
806 806 'short_contact': dbuser.short_contact,
807 807 'full_contact': dbuser.full_contact,
808 808 'full_name': dbuser.full_name,
809 809 'full_name_or_username': dbuser.full_name_or_username,
810 810
811 811 '_api_key': dbuser._api_key,
812 812 '_user_data': dbuser._user_data,
813 813
814 814 'created_on': dbuser.created_on,
815 815 'extern_name': dbuser.extern_name,
816 816 'extern_type': dbuser.extern_type,
817 817
818 818 'inherit_default_permissions': dbuser.inherit_default_permissions,
819 819
820 820 'language': dbuser.language,
821 821 'last_activity': dbuser.last_activity,
822 822 'last_login': dbuser.last_login,
823 823 'password': dbuser.password,
824 824 }
825 825 auth_user.__dict__.update(attrs)
826 826 except Exception:
827 827 log.error(traceback.format_exc())
828 828 auth_user.is_authenticated = False
829 829 return False
830 830
831 831 return True
832 832
833 833 def has_perm(self, user, perm):
834 834 perm = self._get_perm(perm)
835 835 user = self._get_user(user)
836 836
837 837 return UserToPerm.query().filter(UserToPerm.user == user)\
838 838 .filter(UserToPerm.permission == perm).scalar() is not None
839 839
840 840 def grant_perm(self, user, perm):
841 841 """
842 842 Grant user global permissions
843 843
844 844 :param user:
845 845 :param perm:
846 846 """
847 847 user = self._get_user(user)
848 848 perm = self._get_perm(perm)
849 849 # if this permission is already granted skip it
850 850 _perm = UserToPerm.query()\
851 851 .filter(UserToPerm.user == user)\
852 852 .filter(UserToPerm.permission == perm)\
853 853 .scalar()
854 854 if _perm:
855 855 return
856 856 new = UserToPerm()
857 857 new.user = user
858 858 new.permission = perm
859 859 self.sa.add(new)
860 860 return new
861 861
862 862 def revoke_perm(self, user, perm):
863 863 """
864 864 Revoke users global permissions
865 865
866 866 :param user:
867 867 :param perm:
868 868 """
869 869 user = self._get_user(user)
870 870 perm = self._get_perm(perm)
871 871
872 872 obj = UserToPerm.query()\
873 873 .filter(UserToPerm.user == user)\
874 874 .filter(UserToPerm.permission == perm)\
875 875 .scalar()
876 876 if obj:
877 877 self.sa.delete(obj)
878 878
879 879 def add_extra_email(self, user, email):
880 880 """
881 881 Adds email address to UserEmailMap
882 882
883 883 :param user:
884 884 :param email:
885 885 """
886 886
887 887 user = self._get_user(user)
888 888
889 889 obj = UserEmailMap()
890 890 obj.user = user
891 891 obj.email = email
892 892 self.sa.add(obj)
893 893 return obj
894 894
895 895 def delete_extra_email(self, user, email_id):
896 896 """
897 897 Removes email address from UserEmailMap
898 898
899 899 :param user:
900 900 :param email_id:
901 901 """
902 902 user = self._get_user(user)
903 903 obj = UserEmailMap.query().get(email_id)
904 904 if obj and obj.user_id == user.user_id:
905 905 self.sa.delete(obj)
906 906
907 907 def parse_ip_range(self, ip_range):
908 908 ip_list = []
909 909
910 910 def make_unique(value):
911 911 seen = []
912 912 return [c for c in value if not (c in seen or seen.append(c))]
913 913
914 914 # firsts split by commas
915 915 for ip_range in ip_range.split(','):
916 916 if not ip_range:
917 917 continue
918 918 ip_range = ip_range.strip()
919 919 if '-' in ip_range:
920 920 start_ip, end_ip = ip_range.split('-', 1)
921 921 start_ip = ipaddress.ip_address(safe_str(start_ip.strip()))
922 922 end_ip = ipaddress.ip_address(safe_str(end_ip.strip()))
923 923 parsed_ip_range = []
924 924
925 925 for index in range(int(start_ip), int(end_ip) + 1):
926 926 new_ip = ipaddress.ip_address(index)
927 927 parsed_ip_range.append(str(new_ip))
928 928 ip_list.extend(parsed_ip_range)
929 929 else:
930 930 ip_list.append(ip_range)
931 931
932 932 return make_unique(ip_list)
933 933
934 934 def add_extra_ip(self, user, ip, description=None):
935 935 """
936 936 Adds ip address to UserIpMap
937 937
938 938 :param user:
939 939 :param ip:
940 940 """
941 941
942 942 user = self._get_user(user)
943 943 obj = UserIpMap()
944 944 obj.user = user
945 945 obj.ip_addr = ip
946 946 obj.description = description
947 947 self.sa.add(obj)
948 948 return obj
949 949
950 950 auth_token_role = AuthTokenModel.cls
951 951
952 952 def add_auth_token(self, user, lifetime_minutes, role, description='',
953 953 scope_callback=None):
954 954 """
955 955 Add AuthToken for user.
956 956
957 957 :param user: username/user_id
958 958 :param lifetime_minutes: in minutes the lifetime for token, -1 equals no limit
959 959 :param role: one of AuthTokenModel.cls.ROLE_*
960 960 :param description: optional string description
961 961 """
962 962
963 963 token = AuthTokenModel().create(
964 964 user, description, lifetime_minutes, role)
965 965 if scope_callback and callable(scope_callback):
966 966 # call the callback if we provide, used to attach scope for EE edition
967 967 scope_callback(token)
968 968 return token
969 969
970 970 def delete_extra_ip(self, user, ip_id):
971 971 """
972 972 Removes ip address from UserIpMap
973 973
974 974 :param user:
975 975 :param ip_id:
976 976 """
977 977 user = self._get_user(user)
978 978 obj = UserIpMap.query().get(ip_id)
979 979 if obj and obj.user_id == user.user_id:
980 980 self.sa.delete(obj)
981 981
982 982 def get_accounts_in_creation_order(self, current_user=None):
983 983 """
984 984 Get accounts in order of creation for deactivation for license limits
985 985
986 986 pick currently logged in user, and append to the list in position 0
987 987 pick all super-admins in order of creation date and add it to the list
988 988 pick all other accounts in order of creation and add it to the list.
989 989
990 990 Based on that list, the last accounts can be disabled as they are
991 991 created at the end and don't include any of the super admins as well
992 992 as the current user.
993 993
994 994 :param current_user: optionally current user running this operation
995 995 """
996 996
997 997 if not current_user:
998 998 current_user = get_current_rhodecode_user()
999 999 active_super_admins = [
1000 1000 x.user_id for x in User.query()
1001 1001 .filter(User.user_id != current_user.user_id)
1002 1002 .filter(User.active == true())
1003 1003 .filter(User.admin == true())
1004 1004 .order_by(User.created_on.asc())]
1005 1005
1006 1006 active_regular_users = [
1007 1007 x.user_id for x in User.query()
1008 1008 .filter(User.user_id != current_user.user_id)
1009 1009 .filter(User.active == true())
1010 1010 .filter(User.admin == false())
1011 1011 .order_by(User.created_on.asc())]
1012 1012
1013 1013 list_of_accounts = [current_user.user_id]
1014 1014 list_of_accounts += active_super_admins
1015 1015 list_of_accounts += active_regular_users
1016 1016
1017 1017 return list_of_accounts
1018 1018
1019 1019 def deactivate_last_users(self, expected_users, current_user=None):
1020 1020 """
1021 1021 Deactivate accounts that are over the license limits.
1022 1022 Algorithm of which accounts to disabled is based on the formula:
1023 1023
1024 1024 Get current user, then super admins in creation order, then regular
1025 1025 active users in creation order.
1026 1026
1027 1027 Using that list we mark all accounts from the end of it as inactive.
1028 1028 This way we block only latest created accounts.
1029 1029
1030 1030 :param expected_users: list of users in special order, we deactivate
1031 1031 the end N amount of users from that list
1032 1032 """
1033 1033
1034 1034 list_of_accounts = self.get_accounts_in_creation_order(
1035 1035 current_user=current_user)
1036 1036
1037 1037 for acc_id in list_of_accounts[expected_users + 1:]:
1038 1038 user = User.get(acc_id)
1039 1039 log.info('Deactivating account %s for license unlock', user)
1040 1040 user.active = False
1041 1041 Session().add(user)
1042 1042 Session().commit()
1043 1043
1044 1044 return
1045 1045
1046 1046 def get_user_log(self, user, filter_term):
1047 1047 user_log = UserLog.query()\
1048 1048 .filter(or_(UserLog.user_id == user.user_id,
1049 1049 UserLog.username == user.username))\
1050 1050 .options(joinedload(UserLog.user))\
1051 1051 .options(joinedload(UserLog.repository))\
1052 1052 .order_by(UserLog.action_date.desc())
1053 1053
1054 1054 user_log = user_log_filter(user_log, filter_term)
1055 1055 return user_log
@@ -1,421 +1,422 b''
1 1
2 2 /******************************************************************************
3 3 * *
4 4 * DO NOT CHANGE THIS FILE MANUALLY *
5 5 * *
6 6 * *
7 7 * This file is automatically generated when the app starts up with *
8 8 * generate_js_files = true *
9 9 * *
10 10 * To add a route here pass jsroute=True to the route definition in the app *
11 11 * *
12 12 ******************************************************************************/
13 13 function registerRCRoutes() {
14 14 // routes registration
15 15 pyroutes.register('admin_artifacts', '/_admin/artifacts', []);
16 16 pyroutes.register('admin_artifacts_data', '/_admin/artifacts-data', []);
17 17 pyroutes.register('admin_artifacts_delete', '/_admin/artifacts/%(uid)s/delete', ['uid']);
18 18 pyroutes.register('admin_artifacts_show_all', '/_admin/artifacts', []);
19 19 pyroutes.register('admin_artifacts_show_info', '/_admin/artifacts/%(uid)s', ['uid']);
20 20 pyroutes.register('admin_artifacts_update', '/_admin/artifacts/%(uid)s/update', ['uid']);
21 21 pyroutes.register('admin_audit_log_entry', '/_admin/audit_logs/%(audit_log_id)s', ['audit_log_id']);
22 22 pyroutes.register('admin_audit_logs', '/_admin/audit_logs', []);
23 23 pyroutes.register('admin_automation', '/_admin/automation', []);
24 24 pyroutes.register('admin_automation_update', '/_admin/automation/%(entry_id)s/update', ['entry_id']);
25 25 pyroutes.register('admin_defaults_repositories', '/_admin/defaults/repositories', []);
26 26 pyroutes.register('admin_defaults_repositories_update', '/_admin/defaults/repositories/update', []);
27 27 pyroutes.register('admin_home', '/_admin', []);
28 28 pyroutes.register('admin_permissions_application', '/_admin/permissions/application', []);
29 29 pyroutes.register('admin_permissions_application_update', '/_admin/permissions/application/update', []);
30 30 pyroutes.register('admin_permissions_auth_token_access', '/_admin/permissions/auth_token_access', []);
31 31 pyroutes.register('admin_permissions_branch', '/_admin/permissions/branch', []);
32 32 pyroutes.register('admin_permissions_branch_update', '/_admin/permissions/branch/update', []);
33 33 pyroutes.register('admin_permissions_global', '/_admin/permissions/global', []);
34 34 pyroutes.register('admin_permissions_global_update', '/_admin/permissions/global/update', []);
35 35 pyroutes.register('admin_permissions_ips', '/_admin/permissions/ips', []);
36 36 pyroutes.register('admin_permissions_object', '/_admin/permissions/object', []);
37 37 pyroutes.register('admin_permissions_object_update', '/_admin/permissions/object/update', []);
38 38 pyroutes.register('admin_permissions_overview', '/_admin/permissions/overview', []);
39 39 pyroutes.register('admin_permissions_ssh_keys', '/_admin/permissions/ssh_keys', []);
40 40 pyroutes.register('admin_permissions_ssh_keys_data', '/_admin/permissions/ssh_keys/data', []);
41 41 pyroutes.register('admin_permissions_ssh_keys_update', '/_admin/permissions/ssh_keys/update', []);
42 42 pyroutes.register('admin_scheduler', '/_admin/scheduler', []);
43 43 pyroutes.register('admin_scheduler_show_tasks', '/_admin/scheduler/_tasks', []);
44 44 pyroutes.register('admin_settings', '/_admin/settings', []);
45 45 pyroutes.register('admin_settings_email', '/_admin/settings/email', []);
46 46 pyroutes.register('admin_settings_email_update', '/_admin/settings/email/update', []);
47 47 pyroutes.register('admin_settings_exception_tracker', '/_admin/settings/exceptions', []);
48 48 pyroutes.register('admin_settings_exception_tracker_delete', '/_admin/settings/exceptions/%(exception_id)s/delete', ['exception_id']);
49 49 pyroutes.register('admin_settings_exception_tracker_delete_all', '/_admin/settings/exceptions_delete_all', []);
50 50 pyroutes.register('admin_settings_exception_tracker_show', '/_admin/settings/exceptions/%(exception_id)s', ['exception_id']);
51 51 pyroutes.register('admin_settings_global', '/_admin/settings/global', []);
52 52 pyroutes.register('admin_settings_global_update', '/_admin/settings/global/update', []);
53 53 pyroutes.register('admin_settings_hooks', '/_admin/settings/hooks', []);
54 54 pyroutes.register('admin_settings_hooks_delete', '/_admin/settings/hooks/delete', []);
55 55 pyroutes.register('admin_settings_hooks_update', '/_admin/settings/hooks/update', []);
56 56 pyroutes.register('admin_settings_issuetracker', '/_admin/settings/issue-tracker', []);
57 57 pyroutes.register('admin_settings_issuetracker_delete', '/_admin/settings/issue-tracker/delete', []);
58 58 pyroutes.register('admin_settings_issuetracker_test', '/_admin/settings/issue-tracker/test', []);
59 59 pyroutes.register('admin_settings_issuetracker_update', '/_admin/settings/issue-tracker/update', []);
60 60 pyroutes.register('admin_settings_labs', '/_admin/settings/labs', []);
61 61 pyroutes.register('admin_settings_labs_update', '/_admin/settings/labs/update', []);
62 62 pyroutes.register('admin_settings_license', '/_admin/settings/license', []);
63 63 pyroutes.register('admin_settings_license_unlock', '/_admin/settings/license_unlock', []);
64 64 pyroutes.register('admin_settings_mapping', '/_admin/settings/mapping', []);
65 65 pyroutes.register('admin_settings_mapping_update', '/_admin/settings/mapping/update', []);
66 66 pyroutes.register('admin_settings_open_source', '/_admin/settings/open_source', []);
67 67 pyroutes.register('admin_settings_process_management', '/_admin/settings/process_management', []);
68 68 pyroutes.register('admin_settings_process_management_data', '/_admin/settings/process_management/data', []);
69 69 pyroutes.register('admin_settings_process_management_master_signal', '/_admin/settings/process_management/master_signal', []);
70 70 pyroutes.register('admin_settings_process_management_signal', '/_admin/settings/process_management/signal', []);
71 71 pyroutes.register('admin_settings_scheduler_create', '/_admin/scheduler/create', []);
72 72 pyroutes.register('admin_settings_scheduler_delete', '/_admin/scheduler/%(schedule_id)s/delete', ['schedule_id']);
73 73 pyroutes.register('admin_settings_scheduler_edit', '/_admin/scheduler/%(schedule_id)s', ['schedule_id']);
74 74 pyroutes.register('admin_settings_scheduler_execute', '/_admin/scheduler/%(schedule_id)s/execute', ['schedule_id']);
75 75 pyroutes.register('admin_settings_scheduler_new', '/_admin/scheduler/new', []);
76 76 pyroutes.register('admin_settings_scheduler_update', '/_admin/scheduler/%(schedule_id)s/update', ['schedule_id']);
77 77 pyroutes.register('admin_settings_search', '/_admin/settings/search', []);
78 78 pyroutes.register('admin_settings_sessions', '/_admin/settings/sessions', []);
79 79 pyroutes.register('admin_settings_sessions_cleanup', '/_admin/settings/sessions/cleanup', []);
80 80 pyroutes.register('admin_settings_system', '/_admin/settings/system', []);
81 81 pyroutes.register('admin_settings_system_update', '/_admin/settings/system/updates', []);
82 82 pyroutes.register('admin_settings_update', '/_admin/settings/update', []);
83 83 pyroutes.register('admin_settings_vcs', '/_admin/settings/vcs', []);
84 84 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '/_admin/settings/vcs/svn_generate_cfg', []);
85 85 pyroutes.register('admin_settings_vcs_svn_pattern_delete', '/_admin/settings/vcs/svn_pattern_delete', []);
86 86 pyroutes.register('admin_settings_vcs_update', '/_admin/settings/vcs/update', []);
87 87 pyroutes.register('admin_settings_visual', '/_admin/settings/visual', []);
88 88 pyroutes.register('admin_settings_visual_update', '/_admin/settings/visual/update', []);
89 pyroutes.register('admin_security_modify_allowed_vcs_client_versions', '/_admin/security/modify/allowed_vcs_client_versions', []);
89 90 pyroutes.register('apiv2', '/_admin/api', []);
90 91 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed-atom', ['repo_name']);
91 92 pyroutes.register('atom_feed_home_old', '/%(repo_name)s/feed/atom', ['repo_name']);
92 93 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
93 94 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
94 95 pyroutes.register('branch_remove', '/%(repo_name)s/branches/%(branch_name)s/remove', ['repo_name', 'branch_name']);
95 96 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
96 97 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
97 98 pyroutes.register('channelstream_proxy', '/_channelstream', []);
98 99 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
99 100 pyroutes.register('check_2fa', '/_admin/check_2fa', []);
100 101 pyroutes.register('commit_draft_comments_submit', '/%(repo_name)s/changeset/%(commit_id)s/draft_comments_submit', ['repo_name', 'commit_id']);
101 102 pyroutes.register('debug_style_email', '/_admin/debug_style/email/%(email_id)s', ['email_id']);
102 103 pyroutes.register('debug_style_email_plain_rendered', '/_admin/debug_style/email-rendered/%(email_id)s', ['email_id']);
103 104 pyroutes.register('debug_style_home', '/_admin/debug_style', []);
104 105 pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
105 106 pyroutes.register('download_file', '/_file_store/download/%(fid)s', ['fid']);
106 107 pyroutes.register('download_file_by_token', '/_file_store/token-download/%(_auth_token)s/%(fid)s', ['_auth_token', 'fid']);
107 108 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
108 109 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
109 110 pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);
110 111 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
111 112 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
112 113 pyroutes.register('edit_repo_advanced_hooks', '/%(repo_name)s/settings/advanced/hooks', ['repo_name']);
113 114 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
114 115 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
115 116 pyroutes.register('edit_repo_audit_logs', '/%(repo_name)s/settings/audit_logs', ['repo_name']);
116 117 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
117 118 pyroutes.register('edit_repo_fields', '/%(repo_name)s/settings/fields', ['repo_name']);
118 119 pyroutes.register('edit_repo_fields_create', '/%(repo_name)s/settings/fields/create', ['repo_name']);
119 120 pyroutes.register('edit_repo_fields_delete', '/%(repo_name)s/settings/fields/%(field_id)s/delete', ['repo_name', 'field_id']);
120 121 pyroutes.register('edit_repo_group', '/%(repo_group_name)s/_edit', ['repo_group_name']);
121 122 pyroutes.register('edit_repo_group_advanced', '/%(repo_group_name)s/_settings/advanced', ['repo_group_name']);
122 123 pyroutes.register('edit_repo_group_advanced_delete', '/%(repo_group_name)s/_settings/advanced/delete', ['repo_group_name']);
123 124 pyroutes.register('edit_repo_group_perms', '/%(repo_group_name)s/_settings/permissions', ['repo_group_name']);
124 125 pyroutes.register('edit_repo_group_perms_update', '/%(repo_group_name)s/_settings/permissions/update', ['repo_group_name']);
125 126 pyroutes.register('edit_repo_issuetracker', '/%(repo_name)s/settings/issue_trackers', ['repo_name']);
126 127 pyroutes.register('edit_repo_issuetracker_delete', '/%(repo_name)s/settings/issue_trackers/delete', ['repo_name']);
127 128 pyroutes.register('edit_repo_issuetracker_test', '/%(repo_name)s/settings/issue_trackers/test', ['repo_name']);
128 129 pyroutes.register('edit_repo_issuetracker_update', '/%(repo_name)s/settings/issue_trackers/update', ['repo_name']);
129 130 pyroutes.register('edit_repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
130 131 pyroutes.register('edit_repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
131 132 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
132 133 pyroutes.register('edit_repo_perms_branch', '/%(repo_name)s/settings/branch_permissions', ['repo_name']);
133 134 pyroutes.register('edit_repo_perms_branch_delete', '/%(repo_name)s/settings/branch_permissions/%(rule_id)s/delete', ['repo_name', 'rule_id']);
134 135 pyroutes.register('edit_repo_perms_set_private', '/%(repo_name)s/settings/permissions/set_private', ['repo_name']);
135 136 pyroutes.register('edit_repo_remote', '/%(repo_name)s/settings/remote', ['repo_name']);
136 137 pyroutes.register('edit_repo_remote_pull', '/%(repo_name)s/settings/remote/pull', ['repo_name']);
137 138 pyroutes.register('edit_repo_remote_push', '/%(repo_name)s/settings/remote/push', ['repo_name']);
138 139 pyroutes.register('edit_repo_statistics', '/%(repo_name)s/settings/statistics', ['repo_name']);
139 140 pyroutes.register('edit_repo_statistics_reset', '/%(repo_name)s/settings/statistics/update', ['repo_name']);
140 141 pyroutes.register('edit_repo_strip', '/%(repo_name)s/settings/strip', ['repo_name']);
141 142 pyroutes.register('edit_repo_vcs', '/%(repo_name)s/settings/vcs', ['repo_name']);
142 143 pyroutes.register('edit_repo_vcs_svn_pattern_delete', '/%(repo_name)s/settings/vcs/svn_pattern/delete', ['repo_name']);
143 144 pyroutes.register('edit_repo_vcs_update', '/%(repo_name)s/settings/vcs/update', ['repo_name']);
144 145 pyroutes.register('edit_user_audit_logs', '/_admin/users/%(user_id)s/edit/audit', ['user_id']);
145 146 pyroutes.register('edit_user_audit_logs_download', '/_admin/users/%(user_id)s/edit/audit/download', ['user_id']);
146 147 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
147 148 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
148 149 pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
149 150 pyroutes.register('edit_user_auth_tokens_view', '/_admin/users/%(user_id)s/edit/auth_tokens/view', ['user_id']);
150 151 pyroutes.register('edit_user_caches', '/_admin/users/%(user_id)s/edit/caches', ['user_id']);
151 152 pyroutes.register('edit_user_caches_update', '/_admin/users/%(user_id)s/edit/caches/update', ['user_id']);
152 153 pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']);
153 154 pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']);
154 155 pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']);
155 156 pyroutes.register('edit_user_group', '/_admin/user_groups/%(user_group_id)s/edit', ['user_group_id']);
156 157 pyroutes.register('edit_user_group_advanced', '/_admin/user_groups/%(user_group_id)s/edit/advanced', ['user_group_id']);
157 158 pyroutes.register('edit_user_group_advanced_sync', '/_admin/user_groups/%(user_group_id)s/edit/advanced/sync', ['user_group_id']);
158 159 pyroutes.register('edit_user_group_global_perms', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions', ['user_group_id']);
159 160 pyroutes.register('edit_user_group_global_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions/update', ['user_group_id']);
160 161 pyroutes.register('edit_user_group_perms', '/_admin/user_groups/%(user_group_id)s/edit/permissions', ['user_group_id']);
161 162 pyroutes.register('edit_user_group_perms_summary', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary', ['user_group_id']);
162 163 pyroutes.register('edit_user_group_perms_summary_json', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary/json', ['user_group_id']);
163 164 pyroutes.register('edit_user_group_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/permissions/update', ['user_group_id']);
164 165 pyroutes.register('edit_user_groups_management', '/_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
165 166 pyroutes.register('edit_user_groups_management_updates', '/_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
166 167 pyroutes.register('edit_user_ips', '/_admin/users/%(user_id)s/edit/ips', ['user_id']);
167 168 pyroutes.register('edit_user_ips_add', '/_admin/users/%(user_id)s/edit/ips/new', ['user_id']);
168 169 pyroutes.register('edit_user_ips_delete', '/_admin/users/%(user_id)s/edit/ips/delete', ['user_id']);
169 170 pyroutes.register('edit_user_perms_summary', '/_admin/users/%(user_id)s/edit/permissions_summary', ['user_id']);
170 171 pyroutes.register('edit_user_perms_summary_json', '/_admin/users/%(user_id)s/edit/permissions_summary/json', ['user_id']);
171 172 pyroutes.register('edit_user_ssh_keys', '/_admin/users/%(user_id)s/edit/ssh_keys', ['user_id']);
172 173 pyroutes.register('edit_user_ssh_keys_add', '/_admin/users/%(user_id)s/edit/ssh_keys/new', ['user_id']);
173 174 pyroutes.register('edit_user_ssh_keys_delete', '/_admin/users/%(user_id)s/edit/ssh_keys/delete', ['user_id']);
174 175 pyroutes.register('edit_user_ssh_keys_generate_keypair', '/_admin/users/%(user_id)s/edit/ssh_keys/generate', ['user_id']);
175 176 pyroutes.register('favicon', '/favicon.ico', []);
176 177 pyroutes.register('file_preview', '/_file_preview', []);
177 178 pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']);
178 179 pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']);
179 180 pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']);
180 181 pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']);
181 182 pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/rev/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
182 183 pyroutes.register('gist_show_formatted_path', '/_admin/gists/%(gist_id)s/rev/%(revision)s/%(format)s/%(f_path)s', ['gist_id', 'revision', 'format', 'f_path']);
183 184 pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/rev/%(revision)s', ['gist_id', 'revision']);
184 185 pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']);
185 186 pyroutes.register('gists_create', '/_admin/gists/create', []);
186 187 pyroutes.register('gists_new', '/_admin/gists/new', []);
187 188 pyroutes.register('gists_show', '/_admin/gists', []);
188 189 pyroutes.register('global_integrations_create', '/_admin/integrations/%(integration)s/new', ['integration']);
189 190 pyroutes.register('global_integrations_edit', '/_admin/integrations/%(integration)s/%(integration_id)s', ['integration', 'integration_id']);
190 191 pyroutes.register('global_integrations_home', '/_admin/integrations', []);
191 192 pyroutes.register('global_integrations_list', '/_admin/integrations/%(integration)s', ['integration']);
192 193 pyroutes.register('global_integrations_new', '/_admin/integrations/new', []);
193 194 pyroutes.register('goto_switcher_data', '/_goto_data', []);
194 195 pyroutes.register('home', '/', []);
195 196 pyroutes.register('hovercard_pull_request', '/_hovercard/pull_request/%(pull_request_id)s', ['pull_request_id']);
196 197 pyroutes.register('hovercard_repo_commit', '/_hovercard/commit/%(repo_name)s/%(commit_id)s', ['repo_name', 'commit_id']);
197 198 pyroutes.register('hovercard_user', '/_hovercard/user/%(user_id)s', ['user_id']);
198 199 pyroutes.register('hovercard_user_group', '/_hovercard/user_group/%(user_group_id)s', ['user_group_id']);
199 200 pyroutes.register('hovercard_username', '/_hovercard/username/%(username)s', ['username']);
200 201 pyroutes.register('journal', '/_admin/journal', []);
201 202 pyroutes.register('journal_atom', '/_admin/journal/atom', []);
202 203 pyroutes.register('journal_public', '/_admin/public_journal', []);
203 204 pyroutes.register('journal_public_atom', '/_admin/public_journal/atom', []);
204 205 pyroutes.register('journal_public_atom_old', '/_admin/public_journal_atom', []);
205 206 pyroutes.register('journal_public_rss', '/_admin/public_journal/rss', []);
206 207 pyroutes.register('journal_public_rss_old', '/_admin/public_journal_rss', []);
207 208 pyroutes.register('journal_rss', '/_admin/journal/rss', []);
208 209 pyroutes.register('login', '/_admin/login', []);
209 210 pyroutes.register('logout', '/_admin/logout', []);
210 211 pyroutes.register('main_page_repo_groups_data', '/_home_repo_groups', []);
211 212 pyroutes.register('main_page_repos_data', '/_home_repos', []);
212 213 pyroutes.register('markup_preview', '/_markup_preview', []);
213 214 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
214 215 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
215 216 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
216 217 pyroutes.register('my_account_auth_tokens_view', '/_admin/my_account/auth_tokens/view', []);
217 218 pyroutes.register('my_account_bookmarks', '/_admin/my_account/bookmarks', []);
218 219 pyroutes.register('my_account_bookmarks_update', '/_admin/my_account/bookmarks/update', []);
219 220 pyroutes.register('my_account_configure_2fa', '/_admin/my_account/configure_2fa', []);
220 221 pyroutes.register('my_account_configure_2fa_update', '/_admin/my_account/configure_2fa_update', []);
221 222 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
222 223 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
223 224 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
224 225 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
225 226 pyroutes.register('my_account_external_identity', '/_admin/my_account/external-identity', []);
226 227 pyroutes.register('my_account_external_identity_delete', '/_admin/my_account/external-identity/delete', []);
227 228 pyroutes.register('my_account_goto_bookmark', '/_admin/my_account/bookmark/%(bookmark_id)s', ['bookmark_id']);
228 229 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
229 230 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
230 231 pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []);
231 232 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
232 233 pyroutes.register('my_account_password_update', '/_admin/my_account/password/update', []);
233 234 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
234 235 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
235 236 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
236 237 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
237 238 pyroutes.register('my_account_regenerate_2fa_recovery_codes', '/_admin/my_account/regenerate_recovery_codes', []);
238 239 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
239 240 pyroutes.register('my_account_show_2fa_recovery_codes', '/_admin/my_account/recovery_codes', []);
240 241 pyroutes.register('my_account_ssh_keys', '/_admin/my_account/ssh_keys', []);
241 242 pyroutes.register('my_account_ssh_keys_add', '/_admin/my_account/ssh_keys/new', []);
242 243 pyroutes.register('my_account_ssh_keys_delete', '/_admin/my_account/ssh_keys/delete', []);
243 244 pyroutes.register('my_account_ssh_keys_generate', '/_admin/my_account/ssh_keys/generate', []);
244 245 pyroutes.register('my_account_update', '/_admin/my_account/update', []);
245 246 pyroutes.register('my_account_user_group_membership', '/_admin/my_account/user_group_membership', []);
246 247 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
247 248 pyroutes.register('notifications_delete', '/_admin/notifications/%(notification_id)s/delete', ['notification_id']);
248 249 pyroutes.register('notifications_mark_all_read', '/_admin/notifications_mark_all_read', []);
249 250 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
250 251 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
251 252 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
252 253 pyroutes.register('ops_celery_error_test', '/_admin/ops/error-celery', []);
253 254 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
254 255 pyroutes.register('ops_healthcheck', '/_admin/ops/status', []);
255 256 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
256 257 pyroutes.register('ops_redirect_test', '/_admin/ops/redirect', []);
257 258 pyroutes.register('plugin_admin_chat', '/_admin/plugin_admin_chat/%(action)s', ['action']);
258 259 pyroutes.register('pull_requests_global', '/_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
259 260 pyroutes.register('pull_requests_global_0', '/_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
260 261 pyroutes.register('pull_requests_global_1', '/_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
261 262 pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
262 263 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/delete', ['repo_name', 'pull_request_id', 'comment_id']);
263 264 pyroutes.register('pullrequest_comment_edit', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/edit', ['repo_name', 'pull_request_id', 'comment_id']);
264 265 pyroutes.register('pullrequest_comments', '/%(repo_name)s/pull-request/%(pull_request_id)s/comments', ['repo_name', 'pull_request_id']);
265 266 pyroutes.register('pullrequest_create', '/%(repo_name)s/pull-request/create', ['repo_name']);
266 267 pyroutes.register('pullrequest_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/delete', ['repo_name', 'pull_request_id']);
267 268 pyroutes.register('pullrequest_draft_comments_submit', '/%(repo_name)s/pull-request/%(pull_request_id)s/draft_comments_submit', ['repo_name', 'pull_request_id']);
268 269 pyroutes.register('pullrequest_drafts', '/%(repo_name)s/pull-request/%(pull_request_id)s/drafts', ['repo_name', 'pull_request_id']);
269 270 pyroutes.register('pullrequest_merge', '/%(repo_name)s/pull-request/%(pull_request_id)s/merge', ['repo_name', 'pull_request_id']);
270 271 pyroutes.register('pullrequest_new', '/%(repo_name)s/pull-request/new', ['repo_name']);
271 272 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
272 273 pyroutes.register('pullrequest_repo_targets', '/%(repo_name)s/pull-request/repo-targets', ['repo_name']);
273 274 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
274 275 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
275 276 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
276 277 pyroutes.register('pullrequest_todos', '/%(repo_name)s/pull-request/%(pull_request_id)s/todos', ['repo_name', 'pull_request_id']);
277 278 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s/update', ['repo_name', 'pull_request_id']);
278 279 pyroutes.register('register', '/_admin/register', []);
279 280 pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
280 281 pyroutes.register('repo_artifacts_data', '/%(repo_name)s/artifacts_data', ['repo_name']);
281 282 pyroutes.register('repo_artifacts_delete', '/%(repo_name)s/artifacts/delete/%(uid)s', ['repo_name', 'uid']);
282 283 pyroutes.register('repo_artifacts_get', '/%(repo_name)s/artifacts/download/%(uid)s', ['repo_name', 'uid']);
283 284 pyroutes.register('repo_artifacts_info', '/%(repo_name)s/artifacts/info/%(uid)s', ['repo_name', 'uid']);
284 285 pyroutes.register('repo_artifacts_list', '/%(repo_name)s/artifacts', ['repo_name']);
285 286 pyroutes.register('repo_artifacts_new', '/%(repo_name)s/artifacts/new', ['repo_name']);
286 287 pyroutes.register('repo_artifacts_store', '/%(repo_name)s/artifacts/store', ['repo_name']);
287 288 pyroutes.register('repo_artifacts_stream_script', '/_file_store/stream-upload-script', []);
288 289 pyroutes.register('repo_artifacts_stream_store', '/_file_store/stream-upload', []);
289 290 pyroutes.register('repo_artifacts_update', '/%(repo_name)s/artifacts/update/%(uid)s', ['repo_name', 'uid']);
290 291 pyroutes.register('repo_automation', '/%(repo_name)s/settings/automation', ['repo_name']);
291 292 pyroutes.register('repo_automation_update', '/%(repo_name)s/settings/automation/%(entry_id)s/update', ['repo_name', 'entry_id']);
292 293 pyroutes.register('repo_changelog', '/%(repo_name)s/changelog', ['repo_name']);
293 294 pyroutes.register('repo_changelog_file', '/%(repo_name)s/changelog/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
294 295 pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']);
295 296 pyroutes.register('repo_commit_children', '/%(repo_name)s/changeset_children/%(commit_id)s', ['repo_name', 'commit_id']);
296 297 pyroutes.register('repo_commit_comment_attachment_upload', '/%(repo_name)s/changeset/%(commit_id)s/comment/attachment_upload', ['repo_name', 'commit_id']);
297 298 pyroutes.register('repo_commit_comment_create', '/%(repo_name)s/changeset/%(commit_id)s/comment/create', ['repo_name', 'commit_id']);
298 299 pyroutes.register('repo_commit_comment_delete', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/delete', ['repo_name', 'commit_id', 'comment_id']);
299 300 pyroutes.register('repo_commit_comment_edit', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/edit', ['repo_name', 'commit_id', 'comment_id']);
300 301 pyroutes.register('repo_commit_comment_history_view', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/history_view/%(comment_history_id)s', ['repo_name', 'commit_id', 'comment_id', 'comment_history_id']);
301 302 pyroutes.register('repo_commit_comment_preview', '/%(repo_name)s/changeset/%(commit_id)s/comment/preview', ['repo_name', 'commit_id']);
302 303 pyroutes.register('repo_commit_data', '/%(repo_name)s/changeset-data/%(commit_id)s', ['repo_name', 'commit_id']);
303 304 pyroutes.register('repo_commit_download', '/%(repo_name)s/changeset-download/%(commit_id)s', ['repo_name', 'commit_id']);
304 305 pyroutes.register('repo_commit_parents', '/%(repo_name)s/changeset_parents/%(commit_id)s', ['repo_name', 'commit_id']);
305 306 pyroutes.register('repo_commit_patch', '/%(repo_name)s/changeset-patch/%(commit_id)s', ['repo_name', 'commit_id']);
306 307 pyroutes.register('repo_commit_raw', '/%(repo_name)s/changeset-diff/%(commit_id)s', ['repo_name', 'commit_id']);
307 308 pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
308 309 pyroutes.register('repo_commits', '/%(repo_name)s/commits', ['repo_name']);
309 310 pyroutes.register('repo_commits_elements', '/%(repo_name)s/commits_elements', ['repo_name']);
310 311 pyroutes.register('repo_commits_elements_file', '/%(repo_name)s/commits_elements/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
311 312 pyroutes.register('repo_commits_file', '/%(repo_name)s/commits/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
312 313 pyroutes.register('repo_compare', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']);
313 314 pyroutes.register('repo_compare_select', '/%(repo_name)s/compare', ['repo_name']);
314 315 pyroutes.register('repo_create', '/_admin/repos/create', []);
315 316 pyroutes.register('repo_creating', '/%(repo_name)s/repo_creating', ['repo_name']);
316 317 pyroutes.register('repo_creating_check', '/%(repo_name)s/repo_creating_check', ['repo_name']);
317 318 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
318 319 pyroutes.register('repo_file_authors', '/%(repo_name)s/authors/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
319 320 pyroutes.register('repo_file_download', '/%(repo_name)s/download/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
320 321 pyroutes.register('repo_file_download:legacy', '/%(repo_name)s/rawfile/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
321 322 pyroutes.register('repo_file_history', '/%(repo_name)s/history/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
322 323 pyroutes.register('repo_file_raw', '/%(repo_name)s/raw/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
323 324 pyroutes.register('repo_files', '/%(repo_name)s/files/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
324 325 pyroutes.register('repo_files:annotated', '/%(repo_name)s/annotate/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
325 326 pyroutes.register('repo_files:annotated_previous', '/%(repo_name)s/annotate-previous/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
326 327 pyroutes.register('repo_files:default_commit', '/%(repo_name)s/files', ['repo_name']);
327 328 pyroutes.register('repo_files:default_path', '/%(repo_name)s/files/%(commit_id)s/', ['repo_name', 'commit_id']);
328 329 pyroutes.register('repo_files:rendered', '/%(repo_name)s/render/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
329 330 pyroutes.register('repo_files_add_file', '/%(repo_name)s/add_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
330 331 pyroutes.register('repo_files_check_head', '/%(repo_name)s/check_head/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
331 332 pyroutes.register('repo_files_create_file', '/%(repo_name)s/create_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
332 333 pyroutes.register('repo_files_delete_file', '/%(repo_name)s/delete_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
333 334 pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
334 335 pyroutes.register('repo_files_diff_2way_redirect', '/%(repo_name)s/diff-2way/%(f_path)s', ['repo_name', 'f_path']);
335 336 pyroutes.register('repo_files_edit_file', '/%(repo_name)s/edit_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
336 337 pyroutes.register('repo_files_nodelist', '/%(repo_name)s/nodelist/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
337 338 pyroutes.register('repo_files_remove_file', '/%(repo_name)s/remove_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
338 339 pyroutes.register('repo_files_replace_binary', '/%(repo_name)s/replace_binary/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
339 340 pyroutes.register('repo_files_update_file', '/%(repo_name)s/update_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
340 341 pyroutes.register('repo_files_upload_file', '/%(repo_name)s/upload_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
341 342 pyroutes.register('repo_fork_create', '/%(repo_name)s/fork/create', ['repo_name']);
342 343 pyroutes.register('repo_fork_new', '/%(repo_name)s/fork', ['repo_name']);
343 344 pyroutes.register('repo_forks_data', '/%(repo_name)s/forks/data', ['repo_name']);
344 345 pyroutes.register('repo_forks_show_all', '/%(repo_name)s/forks', ['repo_name']);
345 346 pyroutes.register('repo_group_create', '/_admin/repo_group/create', []);
346 347 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
347 348 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
348 349 pyroutes.register('repo_group_integrations_create', '/%(repo_group_name)s/_settings/integrations/%(integration)s/new', ['repo_group_name', 'integration']);
349 350 pyroutes.register('repo_group_integrations_edit', '/%(repo_group_name)s/_settings/integrations/%(integration)s/%(integration_id)s', ['repo_group_name', 'integration', 'integration_id']);
350 351 pyroutes.register('repo_group_integrations_home', '/%(repo_group_name)s/_settings/integrations', ['repo_group_name']);
351 352 pyroutes.register('repo_group_integrations_list', '/%(repo_group_name)s/_settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
352 353 pyroutes.register('repo_group_integrations_new', '/%(repo_group_name)s/_settings/integrations/new', ['repo_group_name']);
353 354 pyroutes.register('repo_group_list_data', '/_repo_groups', []);
354 355 pyroutes.register('repo_group_new', '/_admin/repo_group/new', []);
355 356 pyroutes.register('repo_groups', '/_admin/repo_groups', []);
356 357 pyroutes.register('repo_groups_data', '/_admin/repo_groups_data', []);
357 358 pyroutes.register('repo_integrations_create', '/%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
358 359 pyroutes.register('repo_integrations_edit', '/%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
359 360 pyroutes.register('repo_integrations_home', '/%(repo_name)s/settings/integrations', ['repo_name']);
360 361 pyroutes.register('repo_integrations_list', '/%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
361 362 pyroutes.register('repo_integrations_new', '/%(repo_name)s/settings/integrations/new', ['repo_name']);
362 363 pyroutes.register('repo_list_data', '/_repos', []);
363 364 pyroutes.register('repo_new', '/_admin/repos/new', []);
364 365 pyroutes.register('repo_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
365 366 pyroutes.register('repo_nodetree_full:default_path', '/%(repo_name)s/nodetree_full/%(commit_id)s/', ['repo_name', 'commit_id']);
366 367 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
367 368 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
368 369 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
369 370 pyroutes.register('repo_reviewers_review_rule_delete', '/%(repo_name)s/settings/review/rules/%(rule_id)s/delete', ['repo_name', 'rule_id']);
370 371 pyroutes.register('repo_reviewers_review_rule_edit', '/%(repo_name)s/settings/review/rules/%(rule_id)s', ['repo_name', 'rule_id']);
371 372 pyroutes.register('repo_reviewers_review_rule_new', '/%(repo_name)s/settings/review/rules/new', ['repo_name']);
372 373 pyroutes.register('repo_settings_quick_actions', '/%(repo_name)s/settings/quick-action', ['repo_name']);
373 374 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
374 375 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
375 376 pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
376 377 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
377 378 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
378 379 pyroutes.register('repos', '/_admin/repos', []);
379 380 pyroutes.register('repos_data', '/_admin/repos_data', []);
380 381 pyroutes.register('reset_password', '/_admin/password_reset', []);
381 382 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
382 383 pyroutes.register('robots', '/robots.txt', []);
383 384 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed-rss', ['repo_name']);
384 385 pyroutes.register('rss_feed_home_old', '/%(repo_name)s/feed/rss', ['repo_name']);
385 386 pyroutes.register('search', '/_admin/search', []);
386 387 pyroutes.register('search_repo', '/%(repo_name)s/_search', ['repo_name']);
387 388 pyroutes.register('search_repo_alt', '/%(repo_name)s/search', ['repo_name']);
388 389 pyroutes.register('search_repo_group', '/%(repo_group_name)s/_search', ['repo_group_name']);
389 390 pyroutes.register('setup_2fa', '/_admin/setup_2fa', []);
390 391 pyroutes.register('store_user_session_value', '/_store_session_attr', []);
391 392 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
392 393 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
393 394 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
394 395 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
395 396 pyroutes.register('upload_file', '/_file_store/upload', []);
396 397 pyroutes.register('user_autocomplete_data', '/_users', []);
397 398 pyroutes.register('user_create_personal_repo_group', '/_admin/users/%(user_id)s/create_repo_group', ['user_id']);
398 399 pyroutes.register('user_delete', '/_admin/users/%(user_id)s/delete', ['user_id']);
399 400 pyroutes.register('user_disable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_disable', ['user_id']);
400 401 pyroutes.register('user_edit', '/_admin/users/%(user_id)s/edit', ['user_id']);
401 402 pyroutes.register('user_edit_advanced', '/_admin/users/%(user_id)s/edit/advanced', ['user_id']);
402 403 pyroutes.register('user_edit_global_perms', '/_admin/users/%(user_id)s/edit/global_permissions', ['user_id']);
403 404 pyroutes.register('user_edit_global_perms_update', '/_admin/users/%(user_id)s/edit/global_permissions/update', ['user_id']);
404 405 pyroutes.register('user_enable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_enable', ['user_id']);
405 406 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
406 407 pyroutes.register('user_group_members_data', '/_admin/user_groups/%(user_group_id)s/members', ['user_group_id']);
407 408 pyroutes.register('user_group_profile', '/_profile_user_group/%(user_group_name)s', ['user_group_name']);
408 409 pyroutes.register('user_groups', '/_admin/user_groups', []);
409 410 pyroutes.register('user_groups_create', '/_admin/user_groups/create', []);
410 411 pyroutes.register('user_groups_data', '/_admin/user_groups_data', []);
411 412 pyroutes.register('user_groups_delete', '/_admin/user_groups/%(user_group_id)s/delete', ['user_group_id']);
412 413 pyroutes.register('user_groups_new', '/_admin/user_groups/new', []);
413 414 pyroutes.register('user_groups_update', '/_admin/user_groups/%(user_group_id)s/update', ['user_group_id']);
414 415 pyroutes.register('user_notice_dismiss', '/_admin/users/%(user_id)s/notice_dismiss', ['user_id']);
415 416 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
416 417 pyroutes.register('user_update', '/_admin/users/%(user_id)s/update', ['user_id']);
417 418 pyroutes.register('users', '/_admin/users', []);
418 419 pyroutes.register('users_create', '/_admin/users/create', []);
419 420 pyroutes.register('users_data', '/_admin/users_data', []);
420 421 pyroutes.register('users_new', '/_admin/users/new', []);
421 422 }
@@ -1,399 +1,422 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18 import io
19 19 import shlex
20 20
21 21 import math
22 22 import re
23 23 import os
24 24 import datetime
25 25 import logging
26 26 import queue
27 27 import subprocess
28 28
29 29
30 30 from dateutil.parser import parse
31 31 from pyramid.interfaces import IRoutesMapper
32 32 from pyramid.settings import asbool
33 33 from pyramid.path import AssetResolver
34 34 from threading import Thread
35 35
36 36 from rhodecode.config.jsroutes import generate_jsroutes_content
37 37 from rhodecode.lib.base import get_auth_user
38 38 from rhodecode.lib.celerylib.loader import set_celery_conf
39 39
40 40 import rhodecode
41 41
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 def add_renderer_globals(event):
47 47 from rhodecode.lib import helpers
48 48
49 49 # TODO: When executed in pyramid view context the request is not available
50 50 # in the event. Find a better solution to get the request.
51 51 from pyramid.threadlocal import get_current_request
52 52 request = event['request'] or get_current_request()
53 53
54 54 # Add Pyramid translation as '_' to context
55 55 event['_'] = request.translate
56 56 event['_ungettext'] = request.plularize
57 57 event['h'] = helpers
58 58
59 59
60 60 def set_user_lang(event):
61 61 request = event.request
62 62 cur_user = getattr(request, 'user', None)
63 63
64 64 if cur_user:
65 65 user_lang = cur_user.get_instance().user_data.get('language')
66 66 if user_lang:
67 67 log.debug('lang: setting current user:%s language to: %s', cur_user, user_lang)
68 68 event.request._LOCALE_ = user_lang
69 69
70 70
71 71 def update_celery_conf(event):
72 72 log.debug('Setting celery config from new request')
73 73 set_celery_conf(request=event.request, registry=event.request.registry)
74 74
75 75
76 76 def add_request_user_context(event):
77 77 """
78 78 Adds auth user into request context
79 79 """
80 80
81 81 request = event.request
82 82 # access req_id as soon as possible
83 83 req_id = request.req_id
84 84
85 85 if hasattr(request, 'vcs_call'):
86 86 # skip vcs calls
87 87 return
88 88
89 89 if hasattr(request, 'rpc_method'):
90 90 # skip api calls
91 91 return
92 92
93 93 auth_user, auth_token = get_auth_user(request)
94 94 request.user = auth_user
95 95 request.user_auth_token = auth_token
96 96 request.environ['rc_auth_user'] = auth_user
97 97 request.environ['rc_auth_user_id'] = str(auth_user.user_id)
98 98 request.environ['rc_req_id'] = req_id
99 99
100 100
101 101 def reset_log_bucket(event):
102 102 """
103 103 reset the log bucket on new request
104 104 """
105 105 request = event.request
106 106 request.req_id_records_init()
107 107
108 108
109 109 def scan_repositories_if_enabled(event):
110 110 """
111 111 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
112 112 does a repository scan if enabled in the settings.
113 113 """
114
114 115 settings = event.app.registry.settings
115 116 vcs_server_enabled = settings['vcs.server.enable']
116 117 import_on_startup = settings['startup.import_repos']
118
117 119 if vcs_server_enabled and import_on_startup:
118 120 from rhodecode.model.scm import ScmModel
119 121 from rhodecode.lib.utils import repo2db_mapper
120 122 scm = ScmModel()
121 123 repositories = scm.repo_scan(scm.repos_path)
122 124 repo2db_mapper(repositories, remove_obsolete=False)
123 125
124 126
125 127 def write_metadata_if_needed(event):
126 128 """
127 129 Writes upgrade metadata
128 130 """
129 131 import rhodecode
130 132 from rhodecode.lib import system_info
131 133 from rhodecode.lib import ext_json
132 134
133 135 fname = '.rcmetadata.json'
134 136 ini_loc = os.path.dirname(rhodecode.CONFIG.get('__file__'))
135 137 metadata_destination = os.path.join(ini_loc, fname)
136 138
137 139 def get_update_age():
138 140 now = datetime.datetime.utcnow()
139 141
140 142 with open(metadata_destination, 'rb') as f:
141 143 data = ext_json.json.loads(f.read())
142 144 if 'created_on' in data:
143 145 update_date = parse(data['created_on'])
144 146 diff = now - update_date
145 147 return diff.total_seconds() / 60.0
146 148
147 149 return 0
148 150
149 151 def write():
150 152 configuration = system_info.SysInfo(
151 153 system_info.rhodecode_config)()['value']
152 154 license_token = configuration['config']['license_token']
153 155
154 156 setup = dict(
155 157 workers=configuration['config']['server:main'].get(
156 158 'workers', '?'),
157 159 worker_type=configuration['config']['server:main'].get(
158 160 'worker_class', 'sync'),
159 161 )
160 162 dbinfo = system_info.SysInfo(system_info.database_info)()['value']
161 163 del dbinfo['url']
162 164
163 165 metadata = dict(
164 166 desc='upgrade metadata info',
165 167 license_token=license_token,
166 168 created_on=datetime.datetime.utcnow().isoformat(),
167 169 usage=system_info.SysInfo(system_info.usage_info)()['value'],
168 170 platform=system_info.SysInfo(system_info.platform_type)()['value'],
169 171 database=dbinfo,
170 172 cpu=system_info.SysInfo(system_info.cpu)()['value'],
171 173 memory=system_info.SysInfo(system_info.memory)()['value'],
172 174 setup=setup
173 175 )
174 176
175 177 with open(metadata_destination, 'wb') as f:
176 178 f.write(ext_json.json.dumps(metadata))
177 179
178 180 settings = event.app.registry.settings
179 181 if settings.get('metadata.skip'):
180 182 return
181 183
182 184 # only write this every 24h, workers restart caused unwanted delays
183 185 try:
184 186 age_in_min = get_update_age()
185 187 except Exception:
186 188 age_in_min = 0
187 189
188 190 if age_in_min > 60 * 60 * 24:
189 191 return
190 192
191 193 try:
192 194 write()
193 195 except Exception:
194 196 pass
195 197
196 198
197 199 def write_usage_data(event):
198 200 import rhodecode
199 201 from rhodecode.lib import system_info
200 202 from rhodecode.lib import ext_json
201 203
202 204 settings = event.app.registry.settings
203 205 instance_tag = settings.get('metadata.write_usage_tag')
204 206 if not settings.get('metadata.write_usage'):
205 207 return
206 208
207 209 def get_update_age(dest_file):
208 now = datetime.datetime.utcnow()
210 now = datetime.datetime.now(datetime.UTC)
209 211
210 212 with open(dest_file, 'rb') as f:
211 213 data = ext_json.json.loads(f.read())
212 214 if 'created_on' in data:
213 215 update_date = parse(data['created_on'])
214 216 diff = now - update_date
215 217 return math.ceil(diff.total_seconds() / 60.0)
216 218
217 219 return 0
218 220
219 utc_date = datetime.datetime.utcnow()
221 utc_date = datetime.datetime.now(datetime.UTC)
220 222 hour_quarter = int(math.ceil((utc_date.hour + utc_date.minute/60.0) / 6.))
221 fname = '.rc_usage_{date.year}{date.month:02d}{date.day:02d}_{hour}.json'.format(
222 date=utc_date, hour=hour_quarter)
223 fname = f'.rc_usage_{utc_date.year}{utc_date.month:02d}{utc_date.day:02d}_{hour_quarter}.json'
223 224 ini_loc = os.path.dirname(rhodecode.CONFIG.get('__file__'))
224 225
225 226 usage_dir = os.path.join(ini_loc, '.rcusage')
226 227 if not os.path.isdir(usage_dir):
227 228 os.makedirs(usage_dir)
228 229 usage_metadata_destination = os.path.join(usage_dir, fname)
229 230
230 231 try:
231 232 age_in_min = get_update_age(usage_metadata_destination)
232 233 except Exception:
233 234 age_in_min = 0
234 235
235 236 # write every 6th hour
236 237 if age_in_min and age_in_min < 60 * 6:
237 238 log.debug('Usage file created %s minutes ago, skipping (threshold: %s minutes)...',
238 239 age_in_min, 60 * 6)
239 240 return
240 241
241 242 def write(dest_file):
242 243 configuration = system_info.SysInfo(system_info.rhodecode_config)()['value']
243 244 license_token = configuration['config']['license_token']
244 245
245 246 metadata = dict(
246 247 desc='Usage data',
247 248 instance_tag=instance_tag,
248 249 license_token=license_token,
249 250 created_on=datetime.datetime.utcnow().isoformat(),
250 251 usage=system_info.SysInfo(system_info.usage_info)()['value'],
251 252 )
252 253
253 254 with open(dest_file, 'wb') as f:
254 255 f.write(ext_json.formatted_json(metadata))
255 256
256 257 try:
257 258 log.debug('Writing usage file at: %s', usage_metadata_destination)
258 259 write(usage_metadata_destination)
259 260 except Exception:
260 261 pass
261 262
262 263
263 264 def write_js_routes_if_enabled(event):
264 265 registry = event.app.registry
265 266
266 267 mapper = registry.queryUtility(IRoutesMapper)
267 268 _argument_prog = re.compile(r'\{(.*?)\}|:\((.*)\)')
268 269
269 270 def _extract_route_information(route):
270 271 """
271 272 Convert a route into tuple(name, path, args), eg:
272 273 ('show_user', '/profile/%(username)s', ['username'])
273 274 """
274 275
275 276 route_path = route.pattern
276 277 pattern = route.pattern
277 278
278 279 def replace(matchobj):
279 280 if matchobj.group(1):
280 281 return "%%(%s)s" % matchobj.group(1).split(':')[0]
281 282 else:
282 283 return "%%(%s)s" % matchobj.group(2)
283 284
284 285 route_path = _argument_prog.sub(replace, route_path)
285 286
286 287 if not route_path.startswith('/'):
287 288 route_path = f'/{route_path}'
288 289
289 290 return (
290 291 route.name,
291 292 route_path,
292 293 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
293 294 for arg in _argument_prog.findall(pattern)]
294 295 )
295 296
296 297 def get_routes():
297 298 # pyramid routes
298 299 for route in mapper.get_routes():
299 300 if not route.name.startswith('__'):
300 301 yield _extract_route_information(route)
301 302
302 303 if asbool(registry.settings.get('generate_js_files', 'false')):
303 304 static_path = AssetResolver().resolve('rhodecode:public').abspath()
304 305 jsroutes = get_routes()
305 306 jsroutes_file_content = generate_jsroutes_content(jsroutes)
306 307 jsroutes_file_path = os.path.join(
307 308 static_path, 'js', 'rhodecode', 'routes.js')
308 309
309 310 try:
310 311 with open(jsroutes_file_path, 'w', encoding='utf-8') as f:
311 312 f.write(jsroutes_file_content)
312 313 log.debug('generated JS files in %s', jsroutes_file_path)
313 314 except Exception:
314 315 log.exception('Failed to write routes.js into %s', jsroutes_file_path)
315 316
316 317
318 def import_license_if_present(event):
319 """
320 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
321 does a import license key based on a presence of the file.
322 """
323 settings = event.app.registry.settings
324
325 rhodecode_edition_id = settings.get('rhodecode.edition_id')
326 license_file_path = settings.get('license.import_path')
327 force = settings.get('license.import_path_mode') == 'force'
328
329 if license_file_path and rhodecode_edition_id == 'EE':
330 log.debug('license.import_path= is set importing license from %s', license_file_path)
331 from rhodecode.model.meta import Session
332 from rhodecode.model.license import apply_license_from_file
333 try:
334 apply_license_from_file(license_file_path, force=force)
335 Session().commit()
336 except OSError:
337 log.exception('Failed to import license from %s, make sure this file exists', license_file_path)
338
339
317 340 class Subscriber(object):
318 341 """
319 342 Base class for subscribers to the pyramid event system.
320 343 """
321 344 def __call__(self, event):
322 345 self.run(event)
323 346
324 347 def run(self, event):
325 348 raise NotImplementedError('Subclass has to implement this.')
326 349
327 350
328 351 class AsyncSubscriber(Subscriber):
329 352 """
330 353 Subscriber that handles the execution of events in a separate task to not
331 354 block the execution of the code which triggers the event. It puts the
332 355 received events into a queue from which the worker process takes them in
333 356 order.
334 357 """
335 358 def __init__(self):
336 359 self._stop = False
337 360 self._eventq = queue.Queue()
338 361 self._worker = self.create_worker()
339 362 self._worker.start()
340 363
341 364 def __call__(self, event):
342 365 self._eventq.put(event)
343 366
344 367 def create_worker(self):
345 368 worker = Thread(target=self.do_work)
346 369 worker.daemon = True
347 370 return worker
348 371
349 372 def stop_worker(self):
350 373 self._stop = False
351 374 self._eventq.put(None)
352 375 self._worker.join()
353 376
354 377 def do_work(self):
355 378 while not self._stop:
356 379 event = self._eventq.get()
357 380 if event is not None:
358 381 self.run(event)
359 382
360 383
361 384 class AsyncSubprocessSubscriber(AsyncSubscriber):
362 385 """
363 386 Subscriber that uses the subprocess module to execute a command if an
364 387 event is received. Events are handled asynchronously::
365 388
366 389 subscriber = AsyncSubprocessSubscriber('ls -la', timeout=10)
367 390 subscriber(dummyEvent) # running __call__(event)
368 391
369 392 """
370 393
371 394 def __init__(self, cmd, timeout=None):
372 395 if not isinstance(cmd, (list, tuple)):
373 396 cmd = shlex.split(cmd)
374 397 super().__init__()
375 398 self._cmd = cmd
376 399 self._timeout = timeout
377 400
378 401 def run(self, event):
379 402 cmd = self._cmd
380 403 timeout = self._timeout
381 404 log.debug('Executing command %s.', cmd)
382 405
383 406 try:
384 407 output = subprocess.check_output(
385 408 cmd, timeout=timeout, stderr=subprocess.STDOUT)
386 409 log.debug('Command finished %s', cmd)
387 410 if output:
388 411 log.debug('Command output: %s', output)
389 412 except subprocess.TimeoutExpired as e:
390 413 log.exception('Timeout while executing command.')
391 414 if e.output:
392 415 log.error('Command output: %s', e.output)
393 416 except subprocess.CalledProcessError as e:
394 417 log.exception('Error while executing command.')
395 418 if e.output:
396 419 log.error('Command output: %s', e.output)
397 420 except Exception:
398 421 log.exception(
399 422 'Exception while executing command %s.', cmd)
@@ -1,122 +1,127 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${_('Authentication Settings')}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="breadcrumbs_links()"></%def>
11 11
12 12 <%def name="menu_bar_nav()">
13 13 ${self.menu_items(active='admin')}
14 14 </%def>
15 15
16 16 <%def name="menu_bar_subnav()">
17 17 ${self.admin_menu(active='authentication')}
18 18 </%def>
19 19
20 20 <%def name="main()">
21 21
22 22 <div class="box">
23 23
24 24 <div class='sidebar-col-wrapper'>
25 25
26 26 <div class="sidebar">
27 27 <ul class="nav nav-pills nav-stacked">
28 28 % for item in resource.get_root().get_nav_list():
29
29 30 <li ${('class=active' if item == resource else '')}>
30 <a href="${request.resource_path(item, route_name='auth_home')}">${item.display_name}</a>
31 % if getattr(item, 'is_root', False):
32 <a href="${request.resource_path(item, route_name='auth_home')}">${item.display_name}</a>
33 % else:
34 <a style="padding-left: 10px" href="${request.resource_path(item, route_name='auth_home')}">${item.display_name}</a>
35 % endif
31 36 </li>
32 37 % endfor
33 38 </ul>
34 39 </div>
35 40
36 41 <div class="main-content-full-width">
37 42 ${h.secure_form(request.resource_path(resource, route_name='auth_home'), request=request)}
38 43 <div class="panel panel-default">
39 44
40 45 <div class="panel-heading">
41 46 <h3 class="panel-title">${_("Enabled and Available Plugins")}</h3>
42 47 </div>
43 48
44 49 <div class="panel-body">
45 50
46 51
47 52 <div class="label">${_("Ordered Activated Plugins")}</div>
48 53 <div class="textarea text-area editor">
49 54 ${h.textarea('auth_plugins',cols=120,rows=20,class_="medium")}
50 55 </div>
51 56 <div class="field">
52 57 <p class="help-block pre-formatting">${_('List of plugins, separated by commas.'
53 58 '\nThe order of the plugins is also the order in which '
54 59 'RhodeCode Enterprise will try to authenticate a user.')}
55 60 </p>
56 61 </div>
57 62
58 63 <table class="rctable">
59 64 <th>${_('Activate')}</th>
60 65 <th>${_('Plugin Name')}</th>
61 66 <th>${_('Documentation')}</th>
62 67 <th>${_('Plugin ID')}</th>
63 68 <th>${_('Enabled')}</th>
64 69 %for plugin in available_plugins:
65 70 <tr class="${('inactive' if (not plugin.is_active() and plugin.get_id() in enabled_plugins) else '')}">
66 71 <td>
67 72 <span plugin_id="${plugin.get_id()}" class="toggle-plugin btn ${('btn-success' if plugin.get_id() in enabled_plugins else '')}">
68 73 ${(_('activated') if plugin.get_id() in enabled_plugins else _('not active'))}
69 74 </span>
70 75 </td>
71 76 <td>${plugin.get_display_name()}</td>
72 77 <td>
73 78 % if plugin.docs():
74 79 <a href="${plugin.docs()}">docs</a>
75 80 % endif
76 81 </td>
77 82 <td>${plugin.get_id()}</td>
78 83 <td>${h.bool2icon(plugin.is_active(),show_at_false=False)}</td>
79 84 </tr>
80 85 %endfor
81 86 </table>
82 87
83 88 <div class="buttons">
84 89 ${h.submit('save',_('Save'),class_="btn")}
85 90 </div>
86 91 </div>
87 92 </div>
88 93 ${h.end_form()}
89 94 </div>
90 95 </div>
91 96 </div>
92 97
93 98 <script>
94 99 $('.toggle-plugin').click(function(e){
95 100 var auth_plugins_input = $('#auth_plugins');
96 101 var elems = [];
97 102
98 103 $.each(auth_plugins_input.val().split(',') , function (index, element) {
99 104 if (element !== "") {
100 105 elems.push(element.strip())
101 106 }
102 107 });
103 108
104 109 var cur_button = e.currentTarget;
105 110 var plugin_id = $(cur_button).attr('plugin_id');
106 111 if($(cur_button).hasClass('btn-success')){
107 112 elems.splice(elems.indexOf(plugin_id), 1);
108 113 auth_plugins_input.val(elems.join(',\n'));
109 114 $(cur_button).removeClass('btn-success');
110 115 cur_button.innerHTML = _gettext('not active');
111 116 }
112 117 else{
113 118 if (elems.indexOf(plugin_id) === -1) {
114 119 elems.push(plugin_id);
115 120 }
116 121 auth_plugins_input.val(elems.join(',\n'));
117 122 $(cur_button).addClass('btn-success');
118 123 cur_button.innerHTML = _gettext('activated');
119 124 }
120 125 });
121 126 </script>
122 127 </%def>
@@ -1,139 +1,146 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${_('Authentication Settings')}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="breadcrumbs_links()">
11 11 ${h.link_to(_('Admin'),h.route_path('admin_home'))}
12 12 &raquo;
13 13 ${h.link_to(_('Authentication Plugins'),request.resource_path(resource.__parent__, route_name='auth_home'))}
14 14 &raquo;
15 15 ${resource.display_name}
16 16 </%def>
17 17
18 18 <%def name="menu_bar_nav()">
19 19 ${self.menu_items(active='admin')}
20 20 </%def>
21 21
22 22 <%def name="menu_bar_subnav()">
23 23 ${self.admin_menu(active='authentication')}
24 24 </%def>
25 25
26 26 <%def name="main()">
27 27
28 28 <div class="box">
29 29
30 30 <div class='sidebar-col-wrapper'>
31 31
32 32 <div class="sidebar">
33 33 <ul class="nav nav-pills nav-stacked">
34 34 % for item in resource.get_root().get_nav_list():
35 35 <li ${('class=active' if item == resource else '')}>
36 36 <a href="${request.resource_path(item, route_name='auth_home')}">${item.display_name}</a>
37 37 </li>
38 38 % endfor
39 39 </ul>
40 40 </div>
41 41
42 42 <div class="main-content-full-width">
43 43 <div class="panel panel-default">
44 44 <div class="panel-heading">
45 45 <h3 class="panel-title">${_('Plugin')}: ${resource.display_name}</h3>
46 46 </div>
47 47 <div class="panel-body">
48 48 <div class="plugin_form">
49 49 <div class="fields">
50 50 ${h.secure_form(request.resource_path(resource, route_name='auth_home'), request=request)}
51 51 <div class="form">
52 52
53 ## Allow derived templates to add something above the form
54 ## input fields
55 %if hasattr(next, 'above_form_fields'):
56 ${next.above_form_fields()}
57 %endif
58
59 <h4>${_('Plugin Configuration')}</h4>
53 60 %for node in plugin.get_settings_schema():
54 61 <%
55 62 label_to_type = {'label-checkbox': 'bool', 'label-textarea': 'textarea'}
56 63 %>
57 64
58 65 <div class="field">
59 66 <div class="label ${label_to_type.get(node.widget)}"><label for="${node.name}">${node.title}</label></div>
60 67 <div class="input">
61 68 %if node.widget in ["string", "int", "unicode"]:
62 69 ${h.text(node.name, defaults.get(node.name), class_="large")}
63 70 %elif node.widget == "password":
64 71 ${h.password(node.name, defaults.get(node.name), class_="large")}
65 72 %elif node.widget == "bool":
66 73 %if node.name == "global_2fa" and c.rhodecode_edition_id != "EE":
67 74 <input type="checkbox" disabled/>
68 75 <%node.description = _('This feature is available in RhodeCode EE edition only. Contact {sales_email} to obtain a trial license.').format(sales_email='<a href="mailto:sales@rhodecode.com">sales@rhodecode.com</a>')%>
69 76 %else:
70 77 <div class="checkbox" >${h.checkbox(node.name, True, checked=defaults.get(node.name))}</div>
71 78 %endif
72 79 %elif node.widget == "select":
73 80 ${h.select(node.name, defaults.get(node.name), node.validator.choices, class_="select2AuthSetting")}
74 81 %elif node.widget == "select_with_labels":
75 82 ${h.select(node.name, defaults.get(node.name), node.choices, class_="select2AuthSetting")}
76 83 %elif node.widget == "textarea":
77 84 <div class="textarea" style="margin-left: 0px">${h.textarea(node.name, defaults.get(node.name), rows=10)}</div>
78 85 %elif node.widget == "readonly":
79 86 ${node.default}
80 87 %else:
81 88 This field is of type ${node.typ}, which cannot be displayed. Must be one of [string|int|bool|select].
82 89 %endif
83 90
84 91 %if node.name in errors:
85 92 <span class="error-message">${errors.get(node.name)}</span>
86 93 <br />
87 94 %endif
88 95 <p class="help-block pre-formatting">${node.description | n}</p>
89 96 </div>
90 97 </div>
91 98 %endfor
92 99
93 100 ## Allow derived templates to add something below the form
94 101 ## input fields
95 102 %if hasattr(next, 'below_form_fields'):
96 103 ${next.below_form_fields()}
97 104 %endif
98 105
99 106 <div class="buttons">
100 107 ${h.submit('save',_('Save'),class_="btn")}
101 108 </div>
102 109
103 110 </div>
104 111 ${h.end_form()}
105 112 </div>
106 113 </div>
107 114
108 115 % if request.GET.get('schema'):
109 116 ## this is for development and creation of example configurations for documentation
110 117 <pre>
111 118 % for node in plugin.get_settings_schema():
112 119 *option*: `${node.name}` => `${defaults.get(node.name)}`${'\n # '.join(['']+node.description.splitlines())}
113 120
114 121 % endfor
115 122 </pre>
116 123
117 124 % endif
118 125
119 126 </div>
120 127 </div>
121 128 </div>
122 129
123 130 </div>
124 131 </div>
125 132
126 133
127 134 <script>
128 135 $(document).ready(function() {
129 136 var select2Options = {
130 137 containerCssClass: 'drop-menu',
131 138 dropdownCssClass: 'drop-menu-dropdown',
132 139 dropdownAutoWidth: true,
133 140 minimumResultsForSearch: -1
134 141 };
135 142 $('.select2AuthSetting').select2(select2Options);
136 143
137 144 });
138 145 </script>
139 146 </%def>
@@ -1,307 +1,324 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2
3 3 <%
4 4 elems = [
5 5 (_('Repository ID'), c.rhodecode_db_repo.repo_id, '', ''),
6 6 (_('Owner'), lambda:base.gravatar_with_user(c.rhodecode_db_repo.user.email, tooltip=True), '', ''),
7 7 (_('Created on'), h.format_date(c.rhodecode_db_repo.created_on), '', ''),
8 8 (_('Updated on'), h.format_date(c.rhodecode_db_repo.updated_on), '', ''),
9 9 (_('Cached Commit id'), lambda: h.link_to(c.rhodecode_db_repo.changeset_cache.get('short_id'), h.route_path('repo_commit',repo_name=c.repo_name,commit_id=c.rhodecode_db_repo.changeset_cache.get('raw_id'))), '', ''),
10 10 (_('Cached Commit date'), c.rhodecode_db_repo.changeset_cache.get('date'), '', ''),
11 11 (_('Cached Commit data'), lambda: h.link_to('refresh now', h.current_route_path(request, update_commit_cache=1)), '', ''),
12 12 (_('Attached scoped tokens'), len(c.rhodecode_db_repo.scoped_tokens), '', [x.user for x in c.rhodecode_db_repo.scoped_tokens]),
13 13 (_('Pull requests source'), len(c.rhodecode_db_repo.pull_requests_source), '', ['pr_id:{}, repo:{}'.format(x.pull_request_id,x.source_repo.repo_name) for x in c.rhodecode_db_repo.pull_requests_source]),
14 14 (_('Pull requests target'), len(c.rhodecode_db_repo.pull_requests_target), '', ['pr_id:{}, repo:{}'.format(x.pull_request_id,x.target_repo.repo_name) for x in c.rhodecode_db_repo.pull_requests_target]),
15 15 (_('Attached Artifacts'), len(c.rhodecode_db_repo.artifacts), '', ''),
16 16 ]
17 17 %>
18 18
19 19 <div class="panel panel-default">
20 20 <div class="panel-heading" id="advanced-info" >
21 21 <h3 class="panel-title">${_('Repository: %s') % c.rhodecode_db_repo.repo_name} <a class="permalink" href="#advanced-info"></a></h3>
22 22 </div>
23 23 <div class="panel-body">
24 24 ${base.dt_info_panel(elems)}
25 25 </div>
26 26 </div>
27 27
28 28
29 29 <div class="panel panel-default">
30 30 <div class="panel-heading" id="advanced-fork">
31 31 <h3 class="panel-title">${_('Fork Reference')} <a class="permalink" href="#advanced-fork"></a></h3>
32 32 </div>
33 33 <div class="panel-body">
34 34 ${h.secure_form(h.route_path('edit_repo_advanced_fork', repo_name=c.rhodecode_db_repo.repo_name), request=request)}
35 35
36 36 % if c.rhodecode_db_repo.fork:
37 37 <div class="panel-body-title-text">${h.literal(_('This repository is a fork of %(repo_link)s') % {'repo_link': h.link_to_if(c.has_origin_repo_read_perm,c.rhodecode_db_repo.fork.repo_name, h.route_path('repo_summary', repo_name=c.rhodecode_db_repo.fork.repo_name))})}
38 38 | <button class="btn btn-link btn-danger" type="submit">Remove fork reference</button></div>
39 39 % endif
40 40
41 41 <div class="field">
42 42 ${h.hidden('id_fork_of')}
43 43 ${h.submit('set_as_fork_%s' % c.rhodecode_db_repo.repo_name,_('Set'),class_="btn btn-small",)}
44 44 </div>
45 45 <div class="field">
46 46 <span class="help-block">${_('Manually set this repository as a fork of another from the list')}</span>
47 47 </div>
48 48 ${h.end_form()}
49 49 </div>
50 50 </div>
51 51
52 52
53 53 <div class="panel panel-default">
54 54 <div class="panel-heading" id="advanced-journal">
55 55 <h3 class="panel-title">${_('Public Journal Visibility')} <a class="permalink" href="#advanced-journal"></a></h3>
56 56 </div>
57 57 <div class="panel-body">
58 58 ${h.secure_form(h.route_path('edit_repo_advanced_journal', repo_name=c.rhodecode_db_repo.repo_name), request=request)}
59 59 <div class="field">
60 60 %if c.in_public_journal:
61 61 <button class="btn btn-small" type="submit">
62 62 ${_('Remove from Public Journal')}
63 63 </button>
64 64 %else:
65 65 <button class="btn btn-small" type="submit">
66 66 ${_('Add to Public Journal')}
67 67 </button>
68 68 %endif
69 69 </div>
70 70 <div class="field" >
71 71 <span class="help-block">${_('All actions made on this repository will be visible to everyone following the public journal.')}</span>
72 72 </div>
73 73 ${h.end_form()}
74 74 </div>
75 75 </div>
76 76
77 77
78 78 <div class="panel panel-default">
79 79 <div class="panel-heading" id="advanced-locking">
80 80 <h3 class="panel-title">${_('Locking state')} <a class="permalink" href="#advanced-locking"></a></h3>
81 81 </div>
82 82 <div class="panel-body">
83 83 ${h.secure_form(h.route_path('edit_repo_advanced_locking', repo_name=c.rhodecode_db_repo.repo_name), request=request)}
84 84
85 85 %if c.rhodecode_db_repo.locked[0]:
86 86 <div class="panel-body-title-text">${'Locked by %s on %s. Lock reason: %s' % (h.person_by_id(c.rhodecode_db_repo.locked[0]),
87 87 h.format_date(h. time_to_datetime(c.rhodecode_db_repo.locked[1])), c.rhodecode_db_repo.locked[2])}</div>
88 88 %else:
89 89 <div class="panel-body-title-text">${_('This Repository is not currently locked.')}</div>
90 90 %endif
91 91
92 92 <div class="field" >
93 93 %if c.rhodecode_db_repo.locked[0]:
94 94 ${h.hidden('set_unlock', '1')}
95 95 <button class="btn btn-small" type="submit"
96 96 onclick="submitConfirm(event, this, _gettext('Confirm to unlock this repository'), _gettext('Unlock'), '${c.rhodecode_db_repo.repo_name}')"
97 97 >
98 98 <i class="icon-unlock"></i>
99 99 ${_('Unlock repository')}
100 100 </button>
101 101 %else:
102 102 ${h.hidden('set_lock', '1')}
103 103 <button class="btn btn-small" type="submit"
104 104 onclick="submitConfirm(event, this, _gettext('Confirm to lock this repository'), _gettext('lock'), '${c.rhodecode_db_repo.repo_name}')"
105 105 >
106 106 <i class="icon-lock"></i>
107 107 ${_('Lock repository')}
108 108 </button>
109 109 %endif
110 110 </div>
111 111 <div class="field" >
112 112 <span class="help-block">
113 113 ${_('Force repository locking. This only works when anonymous access is disabled. Pulling from the repository locks the repository to that user until the same user pushes to that repository again.')}
114 114 </span>
115 115 </div>
116 116 ${h.end_form()}
117 117 </div>
118 118 </div>
119 119
120 120
121 121 <div class="panel panel-default">
122 122 <div class="panel-heading" id="advanced-hooks">
123 123 <h3 class="panel-title">${_('Hooks')} <a class="permalink" href="#advanced-hooks"></a></h3>
124 124 </div>
125 125 <div class="panel-body">
126 126 <table class="rctable">
127 127 <th>${_('Hook type')}</th>
128 128 <th>${_('Hook version')}</th>
129 129 <th>${_('Current version')}</th>
130 130 % if c.ver_info_dict:
131 131 <tr>
132 132 <td>${_('PRE HOOK')}</td>
133 133 <td>${c.ver_info_dict['pre_version']}</td>
134 134 <td>${c.rhodecode_version}</td>
135 135 </tr>
136 136 <tr>
137 137 <td>${_('POST HOOK')}</td>
138 138 <td>${c.ver_info_dict['post_version']}</td>
139 139 <td>${c.rhodecode_version}</td>
140 140 </tr>
141 141 % else:
142 142 <tr>
143 143 <td>${_('Unable to read hook information from VCS Server')}</td>
144 144 </tr>
145 145 % endif
146 146 </table>
147 147
148 148 <a class="btn btn-primary" href="${h.route_path('edit_repo_advanced_hooks', repo_name=c.repo_name)}"
149 149 onclick="return confirm('${_('Confirm to reinstall hooks for this repository.')}');">
150 150 ${_('Update Hooks')}
151 151 </a>
152 152 % if c.hooks_outdated:
153 153 <span class="alert-error" style="padding: 10px">
154 154 ${_('Outdated hooks detected, please update hooks using `Update Hooks` action.')}
155 155 </span>
156 156 % endif
157 157 </div>
158 158 </div>
159 159
160 160 <div class="panel panel-warning">
161 161 <div class="panel-heading" id="advanced-archive">
162 162 <h3 class="panel-title">${_('Archive repository')} <a class="permalink" href="#advanced-archive"></a></h3>
163 163 </div>
164 164 <div class="panel-body">
165 165 ${h.secure_form(h.route_path('edit_repo_advanced_archive', repo_name=c.repo_name), request=request)}
166 166
167 167 <div style="margin: 0 0 20px 0" class="fake-space"></div>
168 168
169 169 <div class="field">
170 170 % if c.rhodecode_db_repo.archived:
171 171 This repository is already archived. Only super-admin users can un-archive this repository.
172 172 % else:
173 173 <button class="btn btn-small btn-warning" type="submit"
174 174 onclick="submitConfirm(event, this, _gettext('Confirm to archive this repository. <br/>This action is irreversible !'), _gettext('Archive'), '${c.rhodecode_db_repo.repo_name}')"
175 175 >
176 176 ${_('Archive this repository')}
177 177 </button>
178 178 % endif
179 179
180 180 </div>
181 181 <div class="field">
182 182 <span class="help-block">
183 183 <strong>
184 184 ${_('This action is irreversible')} !
185 185 </strong><br/>
186 186 ${_('Archiving the repository will make it entirely read-only. The repository cannot be committed to.'
187 187 'It is hidden from the search results and dashboard. ')}
188 188 </span>
189 189 </div>
190 190
191 191 ${h.end_form()}
192 192 </div>
193 193 </div>
194 194
195 195
196 196 <div class="panel panel-danger">
197 197 <div class="panel-heading" id="advanced-delete">
198 198 <h3 class="panel-title">${_('Delete repository')} <a class="permalink" href="#advanced-delete"></a></h3>
199 199 </div>
200 200 <div class="panel-body">
201 201 ${h.secure_form(h.route_path('edit_repo_advanced_delete', repo_name=c.repo_name), request=request)}
202 202 <table class="display">
203 203 <tr>
204 204 <td>
205 205 ${_ungettext('This repository has %s fork.', 'This repository has %s forks.', c.rhodecode_db_repo.forks.count()) % c.rhodecode_db_repo.forks.count()}
206 206 </td>
207 207 <td>
208 208 %if c.rhodecode_db_repo.forks.count():
209 209 <input type="radio" name="forks" value="detach_forks" checked="checked"/> <label for="forks">${_('Detach forks')}</label>
210 210 %endif
211 211 </td>
212 212 <td>
213 213 %if c.rhodecode_db_repo.forks.count():
214 214 <input type="radio" name="forks" value="delete_forks"/> <label for="forks">${_('Delete forks')}</label>
215 215 %endif
216 216 </td>
217 217 </tr>
218
218 219 <% attached_prs = len(c.rhodecode_db_repo.pull_requests_source + c.rhodecode_db_repo.pull_requests_target) %>
219 220 % if c.rhodecode_db_repo.pull_requests_source or c.rhodecode_db_repo.pull_requests_target:
220 221 <tr>
221 222 <td>
222 223 ${_ungettext('This repository has %s attached pull request.', 'This repository has %s attached pull requests.', attached_prs) % attached_prs}
223 224 <br/>
224 ${_('Consider to archive this repository instead.')}
225 <br/>
226 <strong>${_('Consider to archive this repository instead.')}</strong>
225 227 </td>
226 228 <td></td>
227 229 <td></td>
228 230 </tr>
229 231 % endif
232
233 <% attached_artifacts = len(c.rhodecode_db_repo.artifacts) %>
234 % if attached_artifacts:
235 <tr>
236 <td>
237 ${_ungettext('This repository has %s attached artifact.', 'This repository has %s attached artifacts.', attached_artifacts) % attached_artifacts}
238 <br/>
239 <br/>
240 <strong>${_('Consider to archive this repository instead.')}</strong>
241 </td>
242 <td></td>
243 <td></td>
244 </tr>
245 % endif
246
230 247 </table>
231 248 <div style="margin: 0 0 20px 0" class="fake-space"></div>
232 249
233 250 <div class="field">
234 251 <button class="btn btn-small btn-danger" type="submit"
235 252 onclick="submitConfirm(event, this, _gettext('Confirm to delete this repository'), _gettext('Delete'), '${c.rhodecode_db_repo.repo_name}')"
236 253 >
237 254 ${_('Delete this repository')}
238 255 </button>
239 256 </div>
240 257 <div class="field">
241 258 <span class="help-block">
242 259 ${_('This repository will be renamed in a special way in order to make it inaccessible to RhodeCode Enterprise and its VCS systems. If you need to fully delete it from the file system, please do it manually, or with rhodecode-cleanup-repos command available in rhodecode-tools.')}
243 260 </span>
244 261 </div>
245 262
246 263 ${h.end_form()}
247 264 </div>
248 265 </div>
249 266
250 267
251 268 <script>
252 269
253 270 var currentRepoId = ${c.rhodecode_db_repo.repo_id};
254 271
255 272 var repoTypeFilter = function(data) {
256 273 var results = [];
257 274
258 275 if (!data.results[0]) {
259 276 return data
260 277 }
261 278
262 279 $.each(data.results[0].children, function() {
263 280 // filter out the SAME repo, it cannot be used as fork of itself
264 281 if (this.repo_id != currentRepoId) {
265 282 this.id = this.repo_id;
266 283 results.push(this)
267 284 }
268 285 });
269 286 data.results[0].children = results;
270 287 return data;
271 288 };
272 289
273 290 $("#id_fork_of").select2({
274 291 cachedDataSource: {},
275 292 minimumInputLength: 2,
276 293 placeholder: "${_('Change repository') if c.rhodecode_db_repo.fork else _('Pick repository')}",
277 294 dropdownAutoWidth: true,
278 295 containerCssClass: "drop-menu",
279 296 dropdownCssClass: "drop-menu-dropdown",
280 297 formatResult: formatRepoResult,
281 298 query: $.debounce(250, function(query){
282 299 self = this;
283 300 var cacheKey = query.term;
284 301 var cachedData = self.cachedDataSource[cacheKey];
285 302
286 303 if (cachedData) {
287 304 query.callback({results: cachedData.results});
288 305 } else {
289 306 $.ajax({
290 307 url: pyroutes.url('repo_list_data'),
291 308 data: {'query': query.term, repo_type: '${c.rhodecode_db_repo.repo_type}'},
292 309 dataType: 'json',
293 310 type: 'GET',
294 311 success: function(data) {
295 312 data = repoTypeFilter(data);
296 313 self.cachedDataSource[cacheKey] = data;
297 314 query.callback({results: data.results});
298 315 },
299 316 error: function(data, textStatus, errorThrown) {
300 317 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
301 318 }
302 319 })
303 320 }
304 321 })
305 322 });
306 323 </script>
307 324
@@ -1,1263 +1,1264 b''
1 1
2 2 <%!
3 3 from rhodecode.lib import html_filters
4 4 %>
5 5
6 6 <%inherit file="root.mako"/>
7 7
8 8 <%include file="/ejs_templates/templates.html"/>
9 9
10 10 <div class="outerwrapper">
11 11 <!-- HEADER -->
12 12 <div class="header">
13 13 <div id="header-inner" class="wrapper">
14 14 <div id="logo">
15 15 <div class="logo-wrapper">
16 16 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-60x60.png')}" alt="RhodeCode"/></a>
17 17 </div>
18 18 % if c.rhodecode_name:
19 19 <div class="branding">
20 20 <a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a>
21 21 </div>
22 22 % endif
23 23 </div>
24 24 <!-- MENU BAR NAV -->
25 25 ${self.menu_bar_nav()}
26 26 <!-- END MENU BAR NAV -->
27 27 </div>
28 28 </div>
29 29 ${self.menu_bar_subnav()}
30 30 <!-- END HEADER -->
31 31
32 32 <!-- CONTENT -->
33 33 <div id="content" class="wrapper">
34 34
35 35 <rhodecode-toast id="notifications"></rhodecode-toast>
36 36
37 37 <div class="main">
38 38 ${next.main()}
39 39 </div>
40 40
41 41 </div>
42 42 <!-- END CONTENT -->
43 43
44 44 </div>
45 45
46 46 <!-- FOOTER -->
47 47 <div id="footer">
48 48 <div id="footer-inner" class="title wrapper">
49 49 <div>
50 50 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
51 51
52 52 <p class="footer-link-right">
53 53 <a class="grey-link-action" href="${h.route_path('home', _query={'showrcid': 1})}">
54 54 RhodeCode
55 55 % if c.visual.show_version:
56 56 ${c.rhodecode_version}
57 57 % endif
58 58 ${c.rhodecode_edition}
59 59 </a> |
60 60
61 61 % if c.visual.rhodecode_support_url:
62 62 <a class="grey-link-action" href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a> |
63 63 <a class="grey-link-action" href="https://docs.rhodecode.com" target="_blank">${_('Documentation')}</a>
64 64 % endif
65 65
66 66 </p>
67 67
68 68 <p class="server-instance" style="display:${sid}">
69 69 ## display hidden instance ID if specially defined
70 70 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
71 71 % if c.rhodecode_instanceid:
72 72 ${_('RhodeCode instance id: {}').format(c.rhodecode_instanceid)}
73 73 % endif
74 74 </p>
75 75 </div>
76 76 </div>
77 77 </div>
78 78
79 79 <!-- END FOOTER -->
80 80
81 81 ### MAKO DEFS ###
82 82
83 83 <%def name="menu_bar_subnav()">
84 84 </%def>
85 85
86 86 <%def name="breadcrumbs(class_='breadcrumbs')">
87 87 <div class="${class_}">
88 88 ${self.breadcrumbs_links()}
89 89 </div>
90 90 </%def>
91 91
92 92 <%def name="admin_menu(active=None)">
93 93
94 94 <div id="context-bar">
95 95 <div class="wrapper">
96 96 <div class="title">
97 97 <div class="title-content">
98 98 <div class="title-main">
99 99 % if c.is_super_admin:
100 100 ${_('Super-admin Panel')}
101 101 % else:
102 102 ${_('Delegated Admin Panel')}
103 103 % endif
104 104 </div>
105 105 </div>
106 106 </div>
107 107
108 108 <ul id="context-pages" class="navigation horizontal-list">
109 109
110 110 ## super-admin case (Top Menu)
111 111 % if c.is_super_admin:
112 112 <li class="${h.is_active('audit_logs', active)}"><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
113 113 <li class="${h.is_active('repositories', active)}"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
114 114 <li class="${h.is_active('repository_groups', active)}"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
115 115 <li class="${h.is_active('users', active)}"><a href="${h.route_path('users')}">${_('Users')}</a></li>
116 116 <li class="${h.is_active('user_groups', active)}"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
117 <li class="${h.is_active('security', active)}"><a href="${h.route_path('admin_security')}">${_('Security')}</a></li>
117 118 <li class="${h.is_active('artifacts', active)}"><a href="${h.route_path('admin_artifacts')}">${_('Artifacts')}</a></li>
118 119 <li class="${h.is_active('automation', active)}"><a href="${h.route_path('admin_automation')}">${_('Automation')}</a></li>
119 120 <li class="${h.is_active('scheduler', active)}"><a href="${h.route_path('admin_scheduler')}">${_('Scheduler')}</a></li>
120 121 <li class="${h.is_active('permissions', active)}"><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
121 122 <li class="${h.is_active('authentication', active)}"><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
122 123 <li class="${h.is_active('integrations', active)}"><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
123 124 <li class="${h.is_active('defaults', active)}"><a href="${h.route_path('admin_defaults_repositories')}">${_('Defaults')}</a></li>
124 125 <li class="${h.is_active('settings', active)}"><a href="${h.route_path('admin_settings')}">${_('Settings')}</a></li>
125 126
126 127 ## delegated admin
127 128 % elif c.is_delegated_admin:
128 129 <%
129 130 repositories=c.auth_user.repositories_admin or c.can_create_repo
130 131 repository_groups=c.auth_user.repository_groups_admin or c.can_create_repo_group
131 132 user_groups=c.auth_user.user_groups_admin or c.can_create_user_group
132 133 %>
133 134
134 135 %if repositories:
135 136 <li class="${h.is_active('repositories', active)} local-admin-repos"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
136 137 %endif
137 138 %if repository_groups:
138 139 <li class="${h.is_active('repository_groups', active)} local-admin-repo-groups"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
139 140 %endif
140 141 %if user_groups:
141 142 <li class="${h.is_active('user_groups', active)} local-admin-user-groups"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
142 143 %endif
143 144 % endif
144 145 </ul>
145 146
146 147 </div>
147 148 <div class="clear"></div>
148 149 </div>
149 150 </%def>
150 151
151 152 <%def name="dt_info_panel(elements)">
152 153 <dl class="dl-horizontal">
153 154 %for dt, dd, title, show_items in elements:
154 155 <dt>${dt}:</dt>
155 156 <dd title="${h.tooltip(title)}">
156 157 %if callable(dd):
157 158 ## allow lazy evaluation of elements
158 159 ${dd()}
159 160 %else:
160 161 ${dd}
161 162 %endif
162 163 %if show_items:
163 164 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
164 165 %endif
165 166 </dd>
166 167
167 168 %if show_items:
168 169 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
169 170 %for item in show_items:
170 171 <dt></dt>
171 172 <dd>${item}</dd>
172 173 %endfor
173 174 </div>
174 175 %endif
175 176
176 177 %endfor
177 178 </dl>
178 179 </%def>
179 180
180 181 <%def name="tr_info_entry(element)">
181 182 <% key, val, title, show_items = element %>
182 183
183 184 <tr>
184 185 <td style="vertical-align: top">${key}</td>
185 186 <td title="${h.tooltip(title)}">
186 187 %if callable(val):
187 188 ## allow lazy evaluation of elements
188 189 ${val()}
189 190 %else:
190 191 ${val}
191 192 %endif
192 193 %if show_items:
193 194 <div class="collapsable-content" data-toggle="item-${h.md5_safe(h.safe_str(val))[:6]}-details" style="display: none">
194 195 % for item in show_items:
195 196 <dt></dt>
196 197 <dd>${item}</dd>
197 198 % endfor
198 199 </div>
199 200 %endif
200 201 </td>
201 202 <td style="vertical-align: top">
202 203 %if show_items:
203 204 <span class="btn-collapse" data-toggle="item-${h.md5_safe(h.safe_str(val))[:6]}-details">${_('Show More')} </span>
204 205 %endif
205 206 </td>
206 207 </tr>
207 208
208 209 </%def>
209 210
210 211 <%def name="gravatar(email, size=16, tooltip=False, tooltip_alt=None, user=None, extra_class=None)">
211 212 <%
212 213 if size > 16:
213 214 gravatar_class = ['gravatar','gravatar-large']
214 215 else:
215 216 gravatar_class = ['gravatar']
216 217
217 218 data_hovercard_url = ''
218 219 data_hovercard_alt = tooltip_alt.replace('<', '&lt;').replace('>', '&gt;') if tooltip_alt else ''
219 220
220 221 if tooltip:
221 222 gravatar_class += ['tooltip-hovercard']
222 223 if extra_class:
223 224 gravatar_class += extra_class
224 225 if tooltip and user:
225 226 if user.username == h.DEFAULT_USER:
226 227 gravatar_class.pop(-1)
227 228 else:
228 229 data_hovercard_url = request.route_path('hovercard_user', user_id=getattr(user, 'user_id', ''))
229 230 gravatar_class = ' '.join(gravatar_class)
230 231
231 232 %>
232 233 <%doc>
233 234 TODO: johbo: For now we serve double size images to make it smooth
234 235 for retina. This is how it worked until now. Should be replaced
235 236 with a better solution at some point.
236 237 </%doc>
237 238
238 239 <img class="${gravatar_class}" height="${size}" width="${size}" data-hovercard-url="${data_hovercard_url}" data-hovercard-alt="${data_hovercard_alt}" src="${h.gravatar_url(email, size * 2, request=request)}" />
239 240 </%def>
240 241
241 242
242 243 <%def name="gravatar_with_user(contact, size=16, show_disabled=False, tooltip=False, _class='rc-user')">
243 244 <%
244 245 email = h.email_or_none(contact)
245 246 rc_user = h.discover_user(contact)
246 247 %>
247 248
248 249 <div class="${_class}">
249 250 ${self.gravatar(email, size, tooltip=tooltip, tooltip_alt=contact, user=rc_user)}
250 251 <span class="${('user user-disabled' if show_disabled else 'user')}">
251 252 ${h.link_to_user(rc_user or contact)}
252 253 </span>
253 254 </div>
254 255 </%def>
255 256
256 257
257 258 <%def name="user_group_icon(user_group=None, size=16, tooltip=False)">
258 259 <%
259 260 if (size > 16):
260 261 gravatar_class = 'icon-user-group-alt'
261 262 else:
262 263 gravatar_class = 'icon-user-group-alt'
263 264
264 265 if tooltip:
265 266 gravatar_class += ' tooltip-hovercard'
266 267
267 268 data_hovercard_url = request.route_path('hovercard_user_group', user_group_id=user_group.users_group_id)
268 269 %>
269 270 <%doc>
270 271 TODO: johbo: For now we serve double size images to make it smooth
271 272 for retina. This is how it worked until now. Should be replaced
272 273 with a better solution at some point.
273 274 </%doc>
274 275
275 276 <i style="font-size: ${size}px" class="${gravatar_class} x-icon-size-${size}" data-hovercard-url="${data_hovercard_url}"></i>
276 277 </%def>
277 278
278 279 <%def name="repo_page_title(repo_instance)">
279 280 <div class="title-content repo-title">
280 281
281 282 <div class="title-main">
282 283 ## SVN/HG/GIT icons
283 284 %if h.is_hg(repo_instance):
284 285 <i class="icon-hg"></i>
285 286 %endif
286 287 %if h.is_git(repo_instance):
287 288 <i class="icon-git"></i>
288 289 %endif
289 290 %if h.is_svn(repo_instance):
290 291 <i class="icon-svn"></i>
291 292 %endif
292 293
293 294 ## public/private
294 295 %if repo_instance.private:
295 296 <i class="icon-repo-private"></i>
296 297 %else:
297 298 <i class="icon-repo-public"></i>
298 299 %endif
299 300
300 301 ## repo name with group name
301 302 ${h.breadcrumb_repo_link(repo_instance)}
302 303
303 304 ## Context Actions
304 305 <div class="pull-right">
305 306 %if c.rhodecode_user.username != h.DEFAULT_USER:
306 307 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid, _query=dict(auth_token=c.rhodecode_user.feed_token))}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
307 308
308 309 <a href="#WatchRepo" onclick="toggleFollowingRepo(this, templateContext.repo_id); return false" title="${_('Watch this Repository and actions on it in your personalized journal')}" class="btn btn-sm ${('watching' if c.repository_is_user_following else '')}">
309 310 % if c.repository_is_user_following:
310 311 <i class="icon-eye-off"></i>${_('Unwatch')}
311 312 % else:
312 313 <i class="icon-eye"></i>${_('Watch')}
313 314 % endif
314 315
315 316 </a>
316 317 %else:
317 318 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid)}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
318 319 %endif
319 320 </div>
320 321
321 322 </div>
322 323
323 324 ## FORKED
324 325 %if repo_instance.fork:
325 326 <p class="discreet">
326 327 <i class="icon-code-fork"></i> ${_('Fork of')}
327 328 ${h.link_to_if(c.has_origin_repo_read_perm,repo_instance.fork.repo_name, h.route_path('repo_summary', repo_name=repo_instance.fork.repo_name))}
328 329 </p>
329 330 %endif
330 331
331 332 ## IMPORTED FROM REMOTE
332 333 %if repo_instance.clone_uri:
333 334 <p class="discreet">
334 335 <i class="icon-code-fork"></i> ${_('Clone from')}
335 336 <a href="${h.safe_str(h.hide_credentials(repo_instance.clone_uri))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
336 337 </p>
337 338 %endif
338 339
339 340 ## LOCKING STATUS
340 341 %if repo_instance.locked[0]:
341 342 <p class="locking_locked discreet">
342 343 <i class="icon-repo-lock"></i>
343 344 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
344 345 </p>
345 346 %elif repo_instance.enable_locking:
346 347 <p class="locking_unlocked discreet">
347 348 ${_('Repository not locked. Pull repository to lock it.')}
348 349 </p>
349 350 %endif
350 351
351 352 </div>
352 353 </%def>
353 354
354 355 <%def name="repo_menu(active=None)">
355 356 <%
356 357 ## determine if we have "any" option available
357 358 can_lock = h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking
358 359 has_actions = can_lock
359 360
360 361 %>
361 362 % if c.rhodecode_db_repo.archived:
362 363 <div class="alert alert-warning text-center">
363 364 <strong>${_('This repository has been archived. It is now read-only.')}</strong>
364 365 </div>
365 366 % endif
366 367
367 368 <!--- REPO CONTEXT BAR -->
368 369 <div id="context-bar">
369 370 <div class="wrapper">
370 371
371 372 <div class="title">
372 373 ${self.repo_page_title(c.rhodecode_db_repo)}
373 374 </div>
374 375
375 376 <ul id="context-pages" class="navigation horizontal-list">
376 377 <li class="${h.is_active('summary', active)}"><a class="menulink" href="${h.route_path('repo_summary_explicit', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
377 378 <li class="${h.is_active('commits', active)}"><a class="menulink" href="${h.route_path('repo_commits', repo_name=c.repo_name)}"><div class="menulabel">${_('Commits')}</div></a></li>
378 379 <li class="${h.is_active('files', active)}"><a class="menulink" href="${h.repo_files_by_ref_url(c.repo_name, c.rhodecode_db_repo.repo_type, f_path='', ref_name=c.rhodecode_db_repo.landing_ref_name, commit_id='tip', query={'at':c.rhodecode_db_repo.landing_ref_name})}"><div class="menulabel">${_('Files')}</div></a></li>
379 380 <li class="${h.is_active('compare', active)}"><a class="menulink" href="${h.route_path('repo_compare_select',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a></li>
380 381
381 382 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
382 383 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
383 384 <li class="${h.is_active('showpullrequest', active)}">
384 385 <a class="menulink" href="${h.route_path('pullrequest_show_all', repo_name=c.repo_name)}" title="${h.tooltip(_('Show Pull Requests for %s') % c.repo_name)}">
385 386 <div class="menulabel">
386 387 ${_('Pull Requests')} <span class="menulink-counter">${c.repository_pull_requests}</span>
387 388 </div>
388 389 </a>
389 390 </li>
390 391 %endif
391 392
392 393 <li class="${h.is_active('artifacts', active)}">
393 394 <a class="menulink" href="${h.route_path('repo_artifacts_list',repo_name=c.repo_name)}">
394 395 <div class="menulabel">
395 396 ${_('Artifacts')} <span class="menulink-counter">${c.repository_artifacts}</span>
396 397 </div>
397 398 </a>
398 399 </li>
399 400
400 401 %if not c.rhodecode_db_repo.archived and h.HasRepoPermissionAll('repository.admin')(c.repo_name):
401 402 <li class="${h.is_active('settings', active)}"><a class="menulink" href="${h.route_path('edit_repo',repo_name=c.repo_name)}"><div class="menulabel">${_('Repository Settings')}</div></a></li>
402 403 %endif
403 404
404 405 <li class="${h.is_active('options', active)}">
405 406 % if has_actions:
406 407 <a class="menulink dropdown">
407 408 <div class="menulabel">${_('Quick Actions')}<div class="show_more"></div></div>
408 409 </a>
409 410 <ul class="submenu">
410 411 %if can_lock:
411 412 %if c.rhodecode_db_repo.locked[0]:
412 413 <li><a class="locking_del" href="${h.route_path('repo_settings_quick_actions',repo_name=c.repo_name, _query={'action': 'toggle-lock', 'set_unlock': 1})}">${_('Unlock Repository')}</a></li>
413 414 %else:
414 415 <li><a class="locking_add" href="${h.route_path('repo_settings_quick_actions',repo_name=c.repo_name, _query={'action': 'toggle-lock', 'set_lock': 1})}">${_('Lock Repository')}</a></li>
415 416 %endif
416 417 %endif
417 418 </ul>
418 419 % endif
419 420 </li>
420 421
421 422 </ul>
422 423 </div>
423 424 <div class="clear"></div>
424 425 </div>
425 426
426 427 <!--- REPO END CONTEXT BAR -->
427 428
428 429 </%def>
429 430
430 431 <%def name="repo_group_page_title(repo_group_instance)">
431 432 <div class="title-content">
432 433 <div class="title-main">
433 434 ## Repository Group icon
434 435 <i class="icon-repo-group"></i>
435 436
436 437 ## repo name with group name
437 438 ${h.breadcrumb_repo_group_link(repo_group_instance)}
438 439 </div>
439 440
440 441 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
441 442 <div class="repo-group-desc discreet">
442 443 ${dt.repo_group_desc(repo_group_instance.description_safe, repo_group_instance.personal, c.visual.stylify_metatags)}
443 444 </div>
444 445
445 446 </div>
446 447 </%def>
447 448
448 449
449 450 <%def name="repo_group_menu(active=None)">
450 451 <%
451 452 gr_name = c.repo_group.group_name if c.repo_group else None
452 453 # create repositories with write permission on group is set to true
453 454 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
454 455
455 456 %>
456 457
457 458
458 459 <!--- REPO GROUP CONTEXT BAR -->
459 460 <div id="context-bar">
460 461 <div class="wrapper">
461 462 <div class="title">
462 463 ${self.repo_group_page_title(c.repo_group)}
463 464 </div>
464 465
465 466 <ul id="context-pages" class="navigation horizontal-list">
466 467 <li class="${h.is_active('home', active)}">
467 468 <a class="menulink" href="${h.route_path('repo_group_home', repo_group_name=c.repo_group.group_name)}"><div class="menulabel">${_('Group Home')}</div></a>
468 469 </li>
469 470 % if c.is_super_admin or group_admin:
470 471 <li class="${h.is_active('settings', active)}">
471 472 <a class="menulink" href="${h.route_path('edit_repo_group',repo_group_name=c.repo_group.group_name)}" title="${_('You have admin right to this group, and can edit it')}"><div class="menulabel">${_('Group Settings')}</div></a>
472 473 </li>
473 474 % endif
474 475
475 476 </ul>
476 477 </div>
477 478 <div class="clear"></div>
478 479 </div>
479 480
480 481 <!--- REPO GROUP CONTEXT BAR -->
481 482
482 483 </%def>
483 484
484 485
485 486 <%def name="usermenu(active=False)">
486 487 <%
487 488 not_anonymous = c.rhodecode_user.username != h.DEFAULT_USER
488 489
489 490 gr_name = c.repo_group.group_name if (hasattr(c, 'repo_group') and c.repo_group) else None
490 491 # create repositories with write permission on group is set to true
491 492
492 493 can_fork = c.is_super_admin or h.HasPermissionAny('hg.fork.repository')()
493 494 create_on_write = h.HasPermissionAny('hg.create.write_on_repogroup.true')()
494 495 group_write = h.HasRepoGroupPermissionAny('group.write')(gr_name, 'can write into group index page')
495 496 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
496 497
497 498 can_create_repos = c.is_super_admin or c.can_create_repo
498 499 can_create_repo_groups = c.is_super_admin or c.can_create_repo_group
499 500
500 501 can_create_repos_in_group = c.is_super_admin or group_admin or (group_write and create_on_write)
501 502 can_create_repo_groups_in_group = c.is_super_admin or group_admin
502 503 %>
503 504
504 505 % if not_anonymous:
505 506 <%
506 507 default_target_group = dict()
507 508 if c.rhodecode_user.personal_repo_group:
508 509 default_target_group = dict(parent_group=c.rhodecode_user.personal_repo_group.group_id)
509 510 %>
510 511
511 512 ## create action
512 513 <li>
513 514 <a href="#create-actions" onclick="return false;" class="menulink childs">
514 515 <i class="icon-plus-circled"></i>
515 516 </a>
516 517
517 518 <div class="action-menu submenu">
518 519
519 520 <ol>
520 521 ## scope of within a repository
521 522 % if hasattr(c, 'rhodecode_db_repo') and c.rhodecode_db_repo:
522 523 <li class="submenu-title">${_('This Repository')}</li>
523 524 <li>
524 525 <a href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">${_('Create Pull Request')}</a>
525 526 </li>
526 527 % if can_fork:
527 528 <li>
528 529 <a href="${h.route_path('repo_fork_new',repo_name=c.repo_name,_query=default_target_group)}">${_('Fork this repository')}</a>
529 530 </li>
530 531 % endif
531 532 % endif
532 533
533 534 ## scope of within repository groups
534 535 % if hasattr(c, 'repo_group') and c.repo_group and (can_create_repos_in_group or can_create_repo_groups_in_group):
535 536 <li class="submenu-title">${_('This Repository Group')}</li>
536 537
537 538 % if can_create_repos_in_group:
538 539 <li>
539 540 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.repo_group.group_id))}">${_('New Repository')}</a>
540 541 </li>
541 542 % endif
542 543
543 544 % if can_create_repo_groups_in_group:
544 545 <li>
545 546 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.repo_group.group_id))}">${_('New Repository Group')}</a>
546 547 </li>
547 548 % endif
548 549 % endif
549 550
550 551 ## personal group
551 552 % if c.rhodecode_user.personal_repo_group:
552 553 <li class="submenu-title">Personal Group</li>
553 554
554 555 <li>
555 556 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}" >${_('New Repository')} </a>
556 557 </li>
557 558
558 559 <li>
559 560 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}">${_('New Repository Group')} </a>
560 561 </li>
561 562 % endif
562 563
563 564 ## Global actions
564 565 <li class="submenu-title">RhodeCode</li>
565 566 % if can_create_repos:
566 567 <li>
567 568 <a href="${h.route_path('repo_new')}" >${_('New Repository')}</a>
568 569 </li>
569 570 % endif
570 571
571 572 % if can_create_repo_groups:
572 573 <li>
573 574 <a href="${h.route_path('repo_group_new')}" >${_('New Repository Group')}</a>
574 575 </li>
575 576 % endif
576 577
577 578 <li>
578 579 <a href="${h.route_path('gists_new')}">${_('New Gist')}</a>
579 580 </li>
580 581
581 582 </ol>
582 583
583 584 </div>
584 585 </li>
585 586
586 587 ## notifications
587 588 <li>
588 589 <a class="${('empty' if c.unread_notifications == 0 else '')}" href="${h.route_path('notifications_show_all')}">
589 590 ${c.unread_notifications}
590 591 </a>
591 592 </li>
592 593 % endif
593 594
594 595 ## USER MENU
595 596 <li id="quick_login_li" class="${'active' if active else ''}">
596 597 % if c.rhodecode_user.username == h.DEFAULT_USER:
597 598 <a id="quick_login_link" class="menulink childs" href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">
598 599 ${gravatar(c.rhodecode_user.email, 20)}
599 600 <span class="user">
600 601 <span>${_('Sign in')}</span>
601 602 </span>
602 603 </a>
603 604 % else:
604 605 ## logged in user
605 606 <a id="quick_login_link" class="menulink childs">
606 607 ${gravatar(c.rhodecode_user.email, 20)}
607 608 <span class="user">
608 609 <span class="menu_link_user">${c.rhodecode_user.username}</span>
609 610 <div class="show_more"></div>
610 611 </span>
611 612 </a>
612 613 ## subnav with menu for logged in user
613 614 <div class="user-menu submenu">
614 615 <div id="quick_login">
615 616 %if c.rhodecode_user.username != h.DEFAULT_USER:
616 617 <div class="">
617 618 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
618 619 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
619 620 <div class="email">${c.rhodecode_user.email}</div>
620 621 </div>
621 622 <div class="">
622 623 <ol class="links">
623 624 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
624 625 % if c.rhodecode_user.personal_repo_group:
625 626 <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
626 627 % endif
627 628 <li>${h.link_to(_(u'Pull Requests'), h.route_path('my_account_pullrequests'))}</li>
628 629
629 630 % if c.debug_style:
630 631 <li>
631 632 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
632 633 <div class="menulabel">${_('[Style]')}</div>
633 634 </a>
634 635 </li>
635 636 % endif
636 637
637 638 ## bookmark-items
638 639 <li class="bookmark-items">
639 640 ${_('Bookmarks')}
640 641 <div class="pull-right">
641 642 <a href="${h.route_path('my_account_bookmarks')}">
642 643
643 644 <i class="icon-cog"></i>
644 645 </a>
645 646 </div>
646 647 </li>
647 648 % if not c.bookmark_items:
648 649 <li>
649 650 <a href="${h.route_path('my_account_bookmarks')}">${_('No Bookmarks yet.')}</a>
650 651 </li>
651 652 % endif
652 653 % for item in c.bookmark_items:
653 654 <li>
654 655 % if item.repo_id:
655 656 <div>
656 657 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
657 658 <code>${item.position}</code>
658 659 % if item.repo_type == 'hg':
659 660 <i class="icon-hg" title="${_('Repository')}" style="font-size: 16px"></i>
660 661 % elif item.repo_type == 'git':
661 662 <i class="icon-git" title="${_('Repository')}" style="font-size: 16px"></i>
662 663 % elif item.repo_type == 'svn':
663 664 <i class="icon-svn" title="${_('Repository')}" style="font-size: 16px"></i>
664 665 % endif
665 666 ${(item.title or h.shorter(item.repo_name, 30))}
666 667 </a>
667 668 </div>
668 669 % elif item.group_id:
669 670 <div>
670 671 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
671 672 <code>${item.position}</code>
672 673 <i class="icon-repo-group" title="${_('Repository group')}" style="font-size: 14px"></i>
673 674 ${(item.title or h.shorter(item.group_name, 30))}
674 675 </a>
675 676 </div>
676 677 % else:
677 678 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
678 679 <code>${item.position}</code>
679 680 ${item.title}
680 681 </a>
681 682 % endif
682 683 </li>
683 684 % endfor
684 685
685 686 <li class="logout">
686 687 ${h.secure_form(h.route_path('logout'), request=request)}
687 688 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
688 689 ${h.end_form()}
689 690 </li>
690 691 </ol>
691 692 </div>
692 693 %endif
693 694 </div>
694 695 </div>
695 696
696 697 % endif
697 698 </li>
698 699 </%def>
699 700
700 701 <%def name="menu_items(active=None)">
701 702 <%
702 703 notice_messages, notice_level = c.rhodecode_user.get_notice_messages()
703 704 notice_display = 'none' if len(notice_messages) == 0 else ''
704 705 %>
705 706
706 707 <ul id="quick" class="main_nav navigation horizontal-list">
707 708 ## notice box for important system messages
708 709 <li style="display: ${notice_display}">
709 710 <a class="notice-box" href="#openNotice" onclick="$('.notice-messages-container').toggle(); return false">
710 711 <div class="menulabel-notice ${notice_level}" >
711 712 ${len(notice_messages)}
712 713 </div>
713 714 </a>
714 715 </li>
715 716 <div class="notice-messages-container" style="display: none">
716 717 <div class="notice-messages">
717 718 <table class="rctable">
718 719 % for notice in notice_messages:
719 720 <tr id="notice-message-${notice['msg_id']}" class="notice-message-${notice['level']}">
720 721 <td style="vertical-align: text-top; width: 20px">
721 722 <i class="tooltip icon-info notice-color-${notice['level']}" title="${notice['level']}"></i>
722 723 </td>
723 724 <td>
724 725 <span><i class="icon-plus-squared cursor-pointer" onclick="$('#notice-${notice['msg_id']}').toggle()"></i> </span>
725 726 ${notice['subject']}
726 727
727 728 <div id="notice-${notice['msg_id']}" style="display: none">
728 729 ${h.render(notice['body'], renderer='markdown')}
729 730 </div>
730 731 </td>
731 732 <td style="vertical-align: text-top; width: 35px;">
732 733 <a class="tooltip" title="${_('dismiss')}" href="#dismiss" onclick="dismissNotice(${notice['msg_id']});return false">
733 734 <i class="icon-remove icon-filled-red"></i>
734 735 </a>
735 736 </td>
736 737 </tr>
737 738
738 739 % endfor
739 740 </table>
740 741 </div>
741 742 </div>
742 743 ## Main filter
743 744 <li>
744 745 <div class="menulabel main_filter_box">
745 746 <div class="main_filter_input_box">
746 747 <ul class="searchItems">
747 748
748 749 <li class="searchTag searchTagIcon">
749 750 <i class="icon-search"></i>
750 751 </li>
751 752
752 753 % if c.template_context['search_context']['repo_id']:
753 754 <li class="searchTag searchTagFilter searchTagHidable" >
754 755 ##<a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">
755 756 <span class="tag">
756 757 This repo
757 758 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
758 759 </span>
759 760 ##</a>
760 761 </li>
761 762 % elif c.template_context['search_context']['repo_group_id']:
762 763 <li class="searchTag searchTagFilter searchTagHidable">
763 764 ##<a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">
764 765 <span class="tag">
765 766 This group
766 767 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
767 768 </span>
768 769 ##</a>
769 770 </li>
770 771 % endif
771 772
772 773 <li class="searchTagInput">
773 774 <input class="main_filter_input" id="main_filter" size="25" type="text" name="main_filter" placeholder="${_('search / go to...')}" value="" />
774 775 </li>
775 776 <li class="searchTag searchTagHelp">
776 777 <a href="#showFilterHelp" onclick="showMainFilterBox(); return false">?</a>
777 778 </li>
778 779 </ul>
779 780 </div>
780 781 </div>
781 782
782 783 <div id="main_filter_help" style="display: none">
783 784 - Use '/' key to quickly access this field.
784 785
785 786 - Enter a name of repository, or repository group for quick search.
786 787
787 788 - Prefix query to allow special search:
788 789
789 790 <strong>user:</strong>admin, to search for usernames, always global
790 791
791 792 <strong>user_group:</strong>devops, to search for user groups, always global
792 793
793 794 <strong>pr:</strong>303, to search for pull request number, title, or description, always global
794 795
795 796 <strong>commit:</strong>efced4, to search for commits, scoped to repositories or groups
796 797
797 798 <strong>file:</strong>models.py, to search for file paths, scoped to repositories or groups
798 799
799 800 % if c.template_context['search_context']['repo_id']:
800 801 For advanced full text search visit: <a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">repository search</a>
801 802 % elif c.template_context['search_context']['repo_group_id']:
802 803 For advanced full text search visit: <a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">repository group search</a>
803 804 % else:
804 805 For advanced full text search visit: <a href="${h.route_path('search')}">global search</a>
805 806 % endif
806 807 </div>
807 808 </li>
808 809
809 810 ## ROOT MENU
810 811 <li class="${h.is_active('home', active)}">
811 812 <a class="menulink" title="${_('Home')}" href="${h.route_path('home')}">
812 813 <div class="menulabel">${_('Home')}</div>
813 814 </a>
814 815 </li>
815 816
816 817 %if c.rhodecode_user.username != h.DEFAULT_USER:
817 818 <li class="${h.is_active('journal', active)}">
818 819 <a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
819 820 <div class="menulabel">${_('Journal')}</div>
820 821 </a>
821 822 </li>
822 823 %else:
823 824 <li class="${h.is_active('journal', active)}">
824 825 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
825 826 <div class="menulabel">${_('Public journal')}</div>
826 827 </a>
827 828 </li>
828 829 %endif
829 830
830 831 <li class="${h.is_active('gists', active)}">
831 832 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
832 833 <div class="menulabel">${_('Gists')}</div>
833 834 </a>
834 835 </li>
835 836
836 837 % if c.is_super_admin or c.is_delegated_admin:
837 838 <li class="${h.is_active('admin', active)}">
838 839 <a class="menulink childs" title="${_('Admin settings')}" href="${h.route_path('admin_home')}">
839 840 <div class="menulabel">${_('Admin')} </div>
840 841 </a>
841 842 </li>
842 843 % endif
843 844
844 845 ## render extra user menu
845 846 ${usermenu(active=(active=='my_account'))}
846 847
847 848 </ul>
848 849
849 850 <script type="text/javascript">
850 851 var visualShowPublicIcon = "${c.visual.show_public_icon}" == "True";
851 852
852 853 var formatRepoResult = function(result, container, query, escapeMarkup) {
853 854 return function(data, escapeMarkup) {
854 855 if (!data.repo_id){
855 856 return data.text; // optgroup text Repositories
856 857 }
857 858
858 859 var tmpl = '';
859 860 var repoType = data['repo_type'];
860 861 var repoName = data['text'];
861 862
862 863 if(data && data.type == 'repo'){
863 864 if(repoType === 'hg'){
864 865 tmpl += '<i class="icon-hg"></i> ';
865 866 }
866 867 else if(repoType === 'git'){
867 868 tmpl += '<i class="icon-git"></i> ';
868 869 }
869 870 else if(repoType === 'svn'){
870 871 tmpl += '<i class="icon-svn"></i> ';
871 872 }
872 873 if(data['private']){
873 874 tmpl += '<i class="icon-lock" ></i> ';
874 875 }
875 876 else if(visualShowPublicIcon){
876 877 tmpl += '<i class="icon-unlock-alt"></i> ';
877 878 }
878 879 }
879 880 tmpl += escapeMarkup(repoName);
880 881 return tmpl;
881 882
882 883 }(result, escapeMarkup);
883 884 };
884 885
885 886 var formatRepoGroupResult = function(result, container, query, escapeMarkup) {
886 887 return function(data, escapeMarkup) {
887 888 if (!data.repo_group_id){
888 889 return data.text; // optgroup text Repositories
889 890 }
890 891
891 892 var tmpl = '';
892 893 var repoGroupName = data['text'];
893 894
894 895 if(data){
895 896
896 897 tmpl += '<i class="icon-repo-group"></i> ';
897 898
898 899 }
899 900 tmpl += escapeMarkup(repoGroupName);
900 901 return tmpl;
901 902
902 903 }(result, escapeMarkup);
903 904 };
904 905
905 906 var escapeRegExChars = function (value) {
906 907 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
907 908 };
908 909
909 910 var getRepoIcon = function(repo_type) {
910 911 if (repo_type === 'hg') {
911 912 return '<i class="icon-hg"></i> ';
912 913 }
913 914 else if (repo_type === 'git') {
914 915 return '<i class="icon-git"></i> ';
915 916 }
916 917 else if (repo_type === 'svn') {
917 918 return '<i class="icon-svn"></i> ';
918 919 }
919 920 return ''
920 921 };
921 922
922 923 var autocompleteMainFilterFormatResult = function (data, value, org_formatter) {
923 924
924 925 if (value.split(':').length === 2) {
925 926 value = value.split(':')[1]
926 927 }
927 928
928 929 var searchType = data['type'];
929 930 var searchSubType = data['subtype'];
930 931 var valueDisplay = data['value_display'];
931 932 var valueIcon = data['value_icon'];
932 933
933 934 var pattern = '(' + escapeRegExChars(value) + ')';
934 935
935 936 valueDisplay = Select2.util.escapeMarkup(valueDisplay);
936 937
937 938 // highlight match
938 939 if (searchType != 'text') {
939 940 valueDisplay = valueDisplay.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
940 941 }
941 942
942 943 var icon = '';
943 944
944 945 if (searchType === 'hint') {
945 946 icon += '<i class="icon-repo-group"></i> ';
946 947 }
947 948 // full text search/hints
948 949 else if (searchType === 'search') {
949 950 if (valueIcon === undefined) {
950 951 icon += '<i class="icon-more"></i> ';
951 952 } else {
952 953 icon += valueIcon + ' ';
953 954 }
954 955
955 956 if (searchSubType !== undefined && searchSubType == 'repo') {
956 957 valueDisplay += '<div class="pull-right tag">repository</div>';
957 958 }
958 959 else if (searchSubType !== undefined && searchSubType == 'repo_group') {
959 960 valueDisplay += '<div class="pull-right tag">repo group</div>';
960 961 }
961 962 }
962 963 // repository
963 964 else if (searchType === 'repo') {
964 965
965 966 var repoIcon = getRepoIcon(data['repo_type']);
966 967 icon += repoIcon;
967 968
968 969 if (data['private']) {
969 970 icon += '<i class="icon-lock" ></i> ';
970 971 }
971 972 else if (visualShowPublicIcon) {
972 973 icon += '<i class="icon-unlock-alt"></i> ';
973 974 }
974 975 }
975 976 // repository groups
976 977 else if (searchType === 'repo_group') {
977 978 icon += '<i class="icon-repo-group"></i> ';
978 979 }
979 980 // user group
980 981 else if (searchType === 'user_group') {
981 982 icon += '<i class="icon-group"></i> ';
982 983 }
983 984 // user
984 985 else if (searchType === 'user') {
985 986 icon += '<img class="gravatar" src="{0}"/>'.format(data['icon_link']);
986 987 }
987 988 // pull request
988 989 else if (searchType === 'pull_request') {
989 990 icon += '<i class="icon-merge"></i> ';
990 991 }
991 992 // commit
992 993 else if (searchType === 'commit') {
993 994 var repo_data = data['repo_data'];
994 995 var repoIcon = getRepoIcon(repo_data['repository_type']);
995 996 if (repoIcon) {
996 997 icon += repoIcon;
997 998 } else {
998 999 icon += '<i class="icon-tag"></i>';
999 1000 }
1000 1001 }
1001 1002 // file
1002 1003 else if (searchType === 'file') {
1003 1004 var repo_data = data['repo_data'];
1004 1005 var repoIcon = getRepoIcon(repo_data['repository_type']);
1005 1006 if (repoIcon) {
1006 1007 icon += repoIcon;
1007 1008 } else {
1008 1009 icon += '<i class="icon-tag"></i>';
1009 1010 }
1010 1011 }
1011 1012 // generic text
1012 1013 else if (searchType === 'text') {
1013 1014 icon = '';
1014 1015 }
1015 1016
1016 1017 var tmpl = '<div class="ac-container-wrap">{0}{1}</div>';
1017 1018 return tmpl.format(icon, valueDisplay);
1018 1019 };
1019 1020
1020 1021 var handleSelect = function(element, suggestion) {
1021 1022 if (suggestion.type === "hint") {
1022 1023 // we skip action
1023 1024 $('#main_filter').focus();
1024 1025 }
1025 1026 else if (suggestion.type === "text") {
1026 1027 // we skip action
1027 1028 $('#main_filter').focus();
1028 1029
1029 1030 } else {
1030 1031 window.location = suggestion['url'];
1031 1032 }
1032 1033 };
1033 1034
1034 1035 var autocompleteMainFilterResult = function (suggestion, originalQuery, queryLowerCase) {
1035 1036 if (queryLowerCase.split(':').length === 2) {
1036 1037 queryLowerCase = queryLowerCase.split(':')[1]
1037 1038 }
1038 1039 if (suggestion.type === "text") {
1039 1040 // special case we don't want to "skip" display for
1040 1041 return true
1041 1042 }
1042 1043 return suggestion.value_display.toLowerCase().indexOf(queryLowerCase) !== -1;
1043 1044 };
1044 1045
1045 1046 var cleanContext = {
1046 1047 repo_view_type: null,
1047 1048
1048 1049 repo_id: null,
1049 1050 repo_name: "",
1050 1051
1051 1052 repo_group_id: null,
1052 1053 repo_group_name: null
1053 1054 };
1054 1055 var removeGoToFilter = function () {
1055 1056 $('.searchTagHidable').hide();
1056 1057 $('#main_filter').autocomplete(
1057 1058 'setOptions', {params:{search_context: cleanContext}});
1058 1059 };
1059 1060
1060 1061 $('#main_filter').autocomplete({
1061 1062 serviceUrl: pyroutes.url('goto_switcher_data'),
1062 1063 params: {
1063 1064 "search_context": templateContext.search_context
1064 1065 },
1065 1066 minChars:2,
1066 1067 maxHeight:400,
1067 1068 deferRequestBy: 300, //miliseconds
1068 1069 tabDisabled: true,
1069 1070 autoSelectFirst: false,
1070 1071 containerClass: 'autocomplete-qfilter-suggestions',
1071 1072 formatResult: autocompleteMainFilterFormatResult,
1072 1073 lookupFilter: autocompleteMainFilterResult,
1073 1074 onSelect: function (element, suggestion) {
1074 1075 handleSelect(element, suggestion);
1075 1076 return false;
1076 1077 },
1077 1078 onSearchError: function (element, query, jqXHR, textStatus, errorThrown) {
1078 1079 if (jqXHR !== 'abort') {
1079 1080 var message = formatErrorMessage(jqXHR, textStatus, errorThrown);
1080 1081 SwalNoAnimation.fire({
1081 1082 icon: 'error',
1082 1083 title: _gettext('Error during search operation'),
1083 1084 html: '<span style="white-space: pre-line">{0}</span>'.format(message),
1084 1085 }).then(function(result) {
1085 1086 window.location.reload();
1086 1087 })
1087 1088 }
1088 1089 },
1089 1090 onSearchStart: function (params) {
1090 1091 $('.searchTag.searchTagIcon').html('<i class="icon-spin animate-spin"></i>')
1091 1092 },
1092 1093 onSearchComplete: function (query, suggestions) {
1093 1094 $('.searchTag.searchTagIcon').html('<i class="icon-search"></i>')
1094 1095 },
1095 1096 });
1096 1097
1097 1098 showMainFilterBox = function () {
1098 1099 $('#main_filter_help').toggle();
1099 1100 };
1100 1101
1101 1102 $('#main_filter').on('keydown.autocomplete', function (e) {
1102 1103
1103 1104 var BACKSPACE = 8;
1104 1105 var el = $(e.currentTarget);
1105 1106 if(e.which === BACKSPACE){
1106 1107 var inputVal = el.val();
1107 1108 if (inputVal === ""){
1108 1109 removeGoToFilter()
1109 1110 }
1110 1111 }
1111 1112 });
1112 1113
1113 1114 var dismissNotice = function(noticeId) {
1114 1115
1115 1116 var url = pyroutes.url('user_notice_dismiss',
1116 1117 {"user_id": templateContext.rhodecode_user.user_id});
1117 1118
1118 1119 var postData = {
1119 1120 'csrf_token': CSRF_TOKEN,
1120 1121 'notice_id': noticeId,
1121 1122 };
1122 1123
1123 1124 var success = function(response) {
1124 1125 $('#notice-message-' + noticeId).remove();
1125 1126 return false;
1126 1127 };
1127 1128 var failure = function(data, textStatus, xhr) {
1128 1129 alert("error processing request: " + textStatus);
1129 1130 return false;
1130 1131 };
1131 1132 ajaxPOST(url, postData, success, failure);
1132 1133 }
1133 1134
1134 1135 var hideLicenseWarning = function () {
1135 1136 var fingerprint = templateContext.session_attrs.license_fingerprint;
1136 1137 storeUserSessionAttr('rc_user_session_attr.hide_license_warning', fingerprint);
1137 1138 $('#notifications').hide();
1138 1139 }
1139 1140
1140 1141 var hideLicenseError = function () {
1141 1142 var fingerprint = templateContext.session_attrs.license_fingerprint;
1142 1143 storeUserSessionAttr('rc_user_session_attr.hide_license_error', fingerprint);
1143 1144 $('#notifications').hide();
1144 1145 }
1145 1146
1146 1147 </script>
1147 1148 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
1148 1149 </%def>
1149 1150
1150 1151 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
1151 1152 <div class="modal-dialog">
1152 1153 <div class="modal-content">
1153 1154 <div class="modal-header">
1154 1155 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
1155 1156 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
1156 1157 </div>
1157 1158 <div class="modal-body">
1158 1159 <div class="block-left">
1159 1160 <table class="keyboard-mappings">
1160 1161 <tbody>
1161 1162 <tr>
1162 1163 <th></th>
1163 1164 <th>${_('Site-wide shortcuts')}</th>
1164 1165 </tr>
1165 1166 <%
1166 1167 elems = [
1167 1168 ('/', 'Use quick search box'),
1168 1169 ('g h', 'Goto home page'),
1169 1170 ('g g', 'Goto my private gists page'),
1170 1171 ('g G', 'Goto my public gists page'),
1171 1172 ('g 0-9', 'Goto bookmarked items from 0-9'),
1172 1173 ('n r', 'New repository page'),
1173 1174 ('n g', 'New gist page'),
1174 1175 ]
1175 1176 %>
1176 1177 %for key, desc in elems:
1177 1178 <tr>
1178 1179 <td class="keys">
1179 1180 <span class="key tag">${key}</span>
1180 1181 </td>
1181 1182 <td>${desc}</td>
1182 1183 </tr>
1183 1184 %endfor
1184 1185 </tbody>
1185 1186 </table>
1186 1187 </div>
1187 1188 <div class="block-left">
1188 1189 <table class="keyboard-mappings">
1189 1190 <tbody>
1190 1191 <tr>
1191 1192 <th></th>
1192 1193 <th>${_('Repositories')}</th>
1193 1194 </tr>
1194 1195 <%
1195 1196 elems = [
1196 1197 ('g s', 'Goto summary page'),
1197 1198 ('g c', 'Goto changelog page'),
1198 1199 ('g f', 'Goto files page'),
1199 1200 ('g F', 'Goto files page with file search activated'),
1200 1201 ('g p', 'Goto pull requests page'),
1201 1202 ('g o', 'Goto repository settings'),
1202 1203 ('g O', 'Goto repository access permissions settings'),
1203 1204 ('t s', 'Toggle sidebar on some pages'),
1204 1205 ]
1205 1206 %>
1206 1207 %for key, desc in elems:
1207 1208 <tr>
1208 1209 <td class="keys">
1209 1210 <span class="key tag">${key}</span>
1210 1211 </td>
1211 1212 <td>${desc}</td>
1212 1213 </tr>
1213 1214 %endfor
1214 1215 </tbody>
1215 1216 </table>
1216 1217 </div>
1217 1218 </div>
1218 1219 <div class="modal-footer">
1219 1220 </div>
1220 1221 </div><!-- /.modal-content -->
1221 1222 </div><!-- /.modal-dialog -->
1222 1223 </div><!-- /.modal -->
1223 1224
1224 1225
1225 1226 <script type="text/javascript">
1226 1227 (function () {
1227 1228 "use sctrict";
1228 1229
1229 1230 // details block auto-hide menu
1230 1231 $(document).mouseup(function(e) {
1231 1232 var container = $('.details-inline-block');
1232 1233 if (!container.is(e.target) && container.has(e.target).length === 0) {
1233 1234 $('.details-inline-block[open]').removeAttr('open')
1234 1235 }
1235 1236 });
1236 1237
1237 1238 var $sideBar = $('.right-sidebar');
1238 1239 var expanded = $sideBar.hasClass('right-sidebar-expanded');
1239 1240 var sidebarState = templateContext.session_attrs.sidebarState;
1240 1241 var sidebarEnabled = $('aside.right-sidebar').get(0);
1241 1242
1242 1243 if (sidebarState === 'expanded') {
1243 1244 expanded = true
1244 1245 } else if (sidebarState === 'collapsed') {
1245 1246 expanded = false
1246 1247 }
1247 1248 if (sidebarEnabled) {
1248 1249 // show sidebar since it's hidden on load
1249 1250 $('.right-sidebar').show();
1250 1251
1251 1252 // init based on set initial class, or if defined user session attrs
1252 1253 if (expanded) {
1253 1254 window.expandSidebar();
1254 1255 window.updateStickyHeader();
1255 1256
1256 1257 } else {
1257 1258 window.collapseSidebar();
1258 1259 window.updateStickyHeader();
1259 1260 }
1260 1261 }
1261 1262 })()
1262 1263
1263 1264 </script>
@@ -1,345 +1,308 b''
1 1 ## snippet for displaying vcs settings
2 2 ## usage:
3 3 ## <%namespace name="vcss" file="/base/vcssettings.mako"/>
4 4 ## ${vcss.vcs_settings_fields()}
5 5
6 6 <%def name="vcs_settings_fields(suffix='', svn_branch_patterns=None, svn_tag_patterns=None, repo_type=None, display_globals=False, **kwargs)">
7 7 % if display_globals:
8 <div class="panel panel-default">
9 <div class="panel-heading" id="general">
10 <h3 class="panel-title">${_('General')}<a class="permalink" href="#general"></a></h3>
11 </div>
12 <div class="panel-body">
13 <div class="field">
14 <div class="checkbox">
15 ${h.checkbox('web_push_ssl' + suffix, 'True')}
16 <label for="web_push_ssl${suffix}">${_('Require SSL for vcs operations')}</label>
17 </div>
18 <div class="label">
19 <span class="help-block">${_('Activate to set RhodeCode to require SSL for pushing or pulling. If SSL certificate is missing it will return a HTTP Error 406: Not Acceptable.')}</span>
20 </div>
21 </div>
22 </div>
23 </div>
8
24 9 % endif
25 10
26 11 % if display_globals or repo_type in ['git', 'hg']:
27 12 <div class="panel panel-default">
28 13 <div class="panel-heading" id="vcs-hooks-options">
29 14 <h3 class="panel-title">${_('Internal Hooks')}<a class="permalink" href="#vcs-hooks-options"></a></h3>
30 15 </div>
31 16 <div class="panel-body">
32 17 <div class="field">
33 18 <div class="checkbox">
34 19 ${h.checkbox('hooks_changegroup_repo_size' + suffix, 'True', **kwargs)}
35 20 <label for="hooks_changegroup_repo_size${suffix}">${_('Show repository size after push')}</label>
36 21 </div>
37 22
38 23 <div class="label">
39 24 <span class="help-block">${_('Trigger a hook that calculates repository size after each push.')}</span>
40 25 </div>
41 26 <div class="checkbox">
42 27 ${h.checkbox('hooks_changegroup_push_logger' + suffix, 'True', **kwargs)}
43 28 <label for="hooks_changegroup_push_logger${suffix}">${_('Execute pre/post push hooks')}</label>
44 29 </div>
45 30 <div class="label">
46 31 <span class="help-block">${_('Execute Built in pre/post push hooks. This also executes rcextensions hooks.')}</span>
47 32 </div>
48 33 <div class="checkbox">
49 34 ${h.checkbox('hooks_outgoing_pull_logger' + suffix, 'True', **kwargs)}
50 35 <label for="hooks_outgoing_pull_logger${suffix}">${_('Execute pre/post pull hooks')}</label>
51 36 </div>
52 37 <div class="label">
53 38 <span class="help-block">${_('Execute Built in pre/post pull hooks. This also executes rcextensions hooks.')}</span>
54 39 </div>
55 40 </div>
56 41 </div>
57 42 </div>
58 43 % endif
59 44
60 45 % if display_globals or repo_type in ['hg']:
61 46 <div class="panel panel-default">
62 47 <div class="panel-heading" id="vcs-hg-options">
63 48 <h3 class="panel-title">${_('Mercurial Settings')}<a class="permalink" href="#vcs-hg-options"></a></h3>
64 49 </div>
65 50 <div class="panel-body">
66 51 <div class="checkbox">
67 52 ${h.checkbox('extensions_largefiles' + suffix, 'True', **kwargs)}
68 53 <label for="extensions_largefiles${suffix}">${_('Enable largefiles extension')}</label>
69 54 </div>
70 55 <div class="label">
71 56 % if display_globals:
72 57 <span class="help-block">${_('Enable Largefiles extensions for all repositories.')}</span>
73 58 % else:
74 59 <span class="help-block">${_('Enable Largefiles extensions for this repository.')}</span>
75 60 % endif
76 61 </div>
77 62
78 % if display_globals:
79 <div class="field">
80 <div class="input">
81 ${h.text('largefiles_usercache' + suffix, size=59)}
82 </div>
83 </div>
84 <div class="label">
85 <span class="help-block">${_('Filesystem location where Mercurial largefile objects should be stored.')}</span>
86 </div>
87 % endif
88
89 63 <div class="checkbox">
90 64 ${h.checkbox('phases_publish' + suffix, 'True', **kwargs)}
91 65 <label for="phases_publish${suffix}">${_('Set repositories as publishing') if display_globals else _('Set repository as publishing')}</label>
92 66 </div>
93 67 <div class="label">
94 68 <span class="help-block">${_('When this is enabled all commits in the repository are seen as public commits by clients.')}</span>
95 69 </div>
96 70
97 71 <div class="checkbox">
98 72 ${h.checkbox('extensions_evolve' + suffix, 'True', **kwargs)}
99 73 <label for="extensions_evolve${suffix}">${_('Enable Evolve and Topic extension')}</label>
100 74 </div>
101 75 <div class="label">
102 76 % if display_globals:
103 77 <span class="help-block">${_('Enable Evolve and Topic extensions for all repositories.')}</span>
104 78 % else:
105 79 <span class="help-block">${_('Enable Evolve and Topic extensions for this repository.')}</span>
106 80 % endif
107 81 </div>
108 82
109 83 </div>
110 84 </div>
111 85 % endif
112 86
113 87 % if display_globals or repo_type in ['git']:
114 88 <div class="panel panel-default">
115 89 <div class="panel-heading" id="vcs-git-options">
116 90 <h3 class="panel-title">${_('Git Settings')}<a class="permalink" href="#vcs-git-options"></a></h3>
117 91 </div>
118 92 <div class="panel-body">
119 93 <div class="checkbox">
120 94 ${h.checkbox('vcs_git_lfs_enabled' + suffix, 'True', **kwargs)}
121 95 <label for="vcs_git_lfs_enabled${suffix}">${_('Enable lfs extension')}</label>
122 96 </div>
123 97 <div class="label">
124 98 % if display_globals:
125 99 <span class="help-block">${_('Enable lfs extensions for all repositories.')}</span>
126 100 % else:
127 101 <span class="help-block">${_('Enable lfs extensions for this repository.')}</span>
128 102 % endif
129 103 </div>
130
131 % if display_globals:
132 <div class="field">
133 <div class="input">
134 ${h.text('vcs_git_lfs_store_location' + suffix, size=59)}
135 </div>
136 </div>
137 <div class="label">
138 <span class="help-block">${_('Filesystem location where Git lfs objects should be stored.')}</span>
139 </div>
140 % endif
141 104 </div>
142 105 </div>
143 106 % endif
144 107
145 108 % if display_globals or repo_type in ['svn']:
146 109 <div class="panel panel-default">
147 110 <div class="panel-heading" id="vcs-svn-options">
148 111 <h3 class="panel-title">${_('Subversion Settings')}<a class="permalink" href="#vcs-svn-options"></a></h3>
149 112 </div>
150 113 <div class="panel-body">
151 114 % if display_globals:
152 115 <div class="field">
153 116 <div class="content" >
154 117 <label>${_('mod_dav config')}</label><br/>
155 118 <code>path: ${c.svn_config_path}</code>
156 119 </div>
157 120 <br/>
158 121
159 122 <div>
160 123
161 124 % if c.svn_generate_config:
162 125 <span class="buttons">
163 126 <button class="btn btn-primary" id="vcs_svn_generate_cfg">${_('Re-generate Apache Config')}</button>
164 127 </span>
165 128 % endif
166 129 </div>
167 130 </div>
168 131 % endif
169 132
170 133 <div class="field">
171 134 <div class="content" >
172 135 <label>${_('Repository patterns')}</label><br/>
173 136 </div>
174 137 </div>
175 138 <div class="label">
176 139 <span class="help-block">${_('Patterns for identifying SVN branches and tags. For recursive search, use "*". Eg.: "/branches/*"')}</span>
177 140 </div>
178 141
179 142 <div class="field branch_patterns">
180 143 <div class="input" >
181 144 <label>${_('Branches')}:</label><br/>
182 145 </div>
183 146 % if svn_branch_patterns:
184 147 % for branch in svn_branch_patterns:
185 148 <div class="input adjacent" id="${'id%s' % branch.ui_id}">
186 149 ${h.hidden('branch_ui_key' + suffix, branch.ui_key)}
187 150 ${h.text('branch_value_%d' % branch.ui_id + suffix, branch.ui_value, size=59, readonly="readonly", class_='disabled')}
188 151 % if kwargs.get('disabled') != 'disabled':
189 152 <span class="btn btn-x" onclick="ajaxDeletePattern(${branch.ui_id},'${'id%s' % branch.ui_id}')">
190 153 ${_('Delete')}
191 154 </span>
192 155 % endif
193 156 </div>
194 157 % endfor
195 158 %endif
196 159 </div>
197 160 % if kwargs.get('disabled') != 'disabled':
198 161 <div class="field branch_patterns">
199 162 <div class="input" >
200 163 ${h.text('new_svn_branch',size=59,placeholder='New branch pattern')}
201 164 </div>
202 165 </div>
203 166 % endif
204 167 <div class="field tag_patterns">
205 168 <div class="input" >
206 169 <label>${_('Tags')}:</label><br/>
207 170 </div>
208 171 % if svn_tag_patterns:
209 172 % for tag in svn_tag_patterns:
210 173 <div class="input" id="${'id%s' % tag.ui_id + suffix}">
211 174 ${h.hidden('tag_ui_key' + suffix, tag.ui_key)}
212 175 ${h.text('tag_ui_value_new_%d' % tag.ui_id + suffix, tag.ui_value, size=59, readonly="readonly", class_='disabled tag_input')}
213 176 % if kwargs.get('disabled') != 'disabled':
214 177 <span class="btn btn-x" onclick="ajaxDeletePattern(${tag.ui_id},'${'id%s' % tag.ui_id}')">
215 178 ${_('Delete')}
216 179 </span>
217 180 %endif
218 181 </div>
219 182 % endfor
220 183 % endif
221 184 </div>
222 185 % if kwargs.get('disabled') != 'disabled':
223 186 <div class="field tag_patterns">
224 187 <div class="input" >
225 188 ${h.text('new_svn_tag' + suffix, size=59, placeholder='New tag pattern')}
226 189 </div>
227 190 </div>
228 191 %endif
229 192 </div>
230 193 </div>
231 194 % else:
232 195 ${h.hidden('new_svn_branch' + suffix, '')}
233 196 ${h.hidden('new_svn_tag' + suffix, '')}
234 197 % endif
235 198
236 199
237 200 % if display_globals or repo_type in ['hg', 'git']:
238 201 <div class="panel panel-default">
239 202 <div class="panel-heading" id="vcs-pull-requests-options">
240 203 <h3 class="panel-title">${_('Pull Request Settings')}<a class="permalink" href="#vcs-pull-requests-options"></a></h3>
241 204 </div>
242 205 <div class="panel-body">
243 206 <div class="checkbox">
244 207 ${h.checkbox('rhodecode_pr_merge_enabled' + suffix, 'True', **kwargs)}
245 208 <label for="rhodecode_pr_merge_enabled${suffix}">${_('Enable server-side merge for pull requests')}</label>
246 209 </div>
247 210 <div class="label">
248 211 <span class="help-block">${_('Note: when this feature is enabled, it only runs hooks defined in the rcextension package. Custom hooks added on the Admin -> Settings -> Hooks page will not be run when pull requests are automatically merged from the web interface.')}</span>
249 212 </div>
250 213 <div class="checkbox">
251 214 ${h.checkbox('rhodecode_use_outdated_comments' + suffix, 'True', **kwargs)}
252 215 <label for="rhodecode_use_outdated_comments${suffix}">${_('Invalidate and relocate inline comments during update')}</label>
253 216 </div>
254 217 <div class="label">
255 218 <span class="help-block">${_('During the update of a pull request, the position of inline comments will be updated and outdated inline comments will be hidden.')}</span>
256 219 </div>
257 220 </div>
258 221 </div>
259 222 % endif
260 223
261 224 % if display_globals or repo_type in ['hg', 'git', 'svn']:
262 225 <div class="panel panel-default">
263 226 <div class="panel-heading" id="vcs-pull-requests-options">
264 227 <h3 class="panel-title">${_('Diff cache')}<a class="permalink" href="#vcs-pull-requests-options"></a></h3>
265 228 </div>
266 229 <div class="panel-body">
267 230 <div class="checkbox">
268 231 ${h.checkbox('rhodecode_diff_cache' + suffix, 'True', **kwargs)}
269 232 <label for="rhodecode_diff_cache${suffix}">${_('Enable caching diffs for pull requests cache and commits')}</label>
270 233 </div>
271 234 </div>
272 235 </div>
273 236 % endif
274 237
275 238 % if display_globals or repo_type in ['hg',]:
276 239 <div class="panel panel-default">
277 240 <div class="panel-heading" id="vcs-pull-requests-options">
278 241 <h3 class="panel-title">${_('Mercurial Pull Request Settings')}<a class="permalink" href="#vcs-hg-pull-requests-options"></a></h3>
279 242 </div>
280 243 <div class="panel-body">
281 244 ## Specific HG settings
282 245 <div class="checkbox">
283 246 ${h.checkbox('rhodecode_hg_use_rebase_for_merging' + suffix, 'True', **kwargs)}
284 247 <label for="rhodecode_hg_use_rebase_for_merging${suffix}">${_('Use rebase as merge strategy')}</label>
285 248 </div>
286 249 <div class="label">
287 250 <span class="help-block">${_('Use rebase instead of creating a merge commit when merging via web interface.')}</span>
288 251 </div>
289 252
290 253 <div class="checkbox">
291 254 ${h.checkbox('rhodecode_hg_close_branch_before_merging' + suffix, 'True', **kwargs)}
292 255 <label for="rhodecode_hg_close_branch_before_merging{suffix}">${_('Close branch before merging it')}</label>
293 256 </div>
294 257 <div class="label">
295 258 <span class="help-block">${_('Close branch before merging it into destination branch. No effect when rebase strategy is use.')}</span>
296 259 </div>
297 260
298 261
299 262 </div>
300 263 </div>
301 264 % endif
302 265
303 266 % if display_globals or repo_type in ['git']:
304 267 <div class="panel panel-default">
305 268 <div class="panel-heading" id="vcs-pull-requests-options">
306 269 <h3 class="panel-title">${_('Git Pull Request Settings')}<a class="permalink" href="#vcs-git-pull-requests-options"></a></h3>
307 270 </div>
308 271 <div class="panel-body">
309 272 ## <div class="checkbox">
310 273 ## ${h.checkbox('rhodecode_git_use_rebase_for_merging' + suffix, 'True', **kwargs)}
311 274 ## <label for="rhodecode_git_use_rebase_for_merging${suffix}">${_('Use rebase as merge strategy')}</label>
312 275 ## </div>
313 276 ## <div class="label">
314 277 ## <span class="help-block">${_('Use rebase instead of creating a merge commit when merging via web interface.')}</span>
315 278 ## </div>
316 279
317 280 <div class="checkbox">
318 281 ${h.checkbox('rhodecode_git_close_branch_before_merging' + suffix, 'True', **kwargs)}
319 282 <label for="rhodecode_git_close_branch_before_merging{suffix}">${_('Delete branch after merging it')}</label>
320 283 </div>
321 284 <div class="label">
322 285 <span class="help-block">${_('Delete branch after merging it into destination branch.')}</span>
323 286 </div>
324 287 </div>
325 288 </div>
326 289 % endif
327 290
328 291 <script type="text/javascript">
329 292
330 293 $(document).ready(function() {
331 294 /* On click handler for the `Generate Apache Config` button. It sends a
332 295 POST request to trigger the (re)generation of the mod_dav_svn config. */
333 296 $('#vcs_svn_generate_cfg').on('click', function(event) {
334 297 event.preventDefault();
335 298 var url = "${h.route_path('admin_settings_vcs_svn_generate_cfg')}";
336 299 var jqxhr = $.post(url, {'csrf_token': CSRF_TOKEN});
337 300 jqxhr.done(function(data) {
338 301 $.Topic('/notifications').publish(data);
339 302 });
340 303 });
341 304 });
342 305
343 306 </script>
344 307 </%def>
345 308
@@ -1,205 +1,205 b''
1 1
2 2
3 3 # Copyright (C) 2016-2023 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import pytest
23 23
24 24 from rhodecode.tests import no_newline_id_generator
25 25 from rhodecode.config.middleware import sanitize_settings_and_apply_defaults
26 26 from rhodecode.config.settings_maker import SettingsMaker
27 27
28 28
29 29 class TestHelperFunctions(object):
30 30 @pytest.mark.parametrize('raw, expected', [
31 31 ('true', True), (u'true', True),
32 32 ('yes', True), (u'yes', True),
33 33 ('on', True), (u'on', True),
34 34 ('false', False), (u'false', False),
35 35 ('no', False), (u'no', False),
36 36 ('off', False), (u'off', False),
37 37 ('invalid-bool-value', False),
38 38 ('invalid-∫øø@-√å@¨€', False),
39 39 (u'invalid-∫øø@-√å@¨€', False),
40 40 ])
41 41 def test_bool_func_helper(self, raw, expected):
42 42 val = SettingsMaker._bool_func(raw)
43 43 assert val == expected
44 44
45 45 @pytest.mark.parametrize('raw, expected', [
46 46 ('', ''),
47 47 ('test-string', 'test-string'),
48 48 ('CaSe-TeSt', 'case-test'),
49 49 ('test-string-烩€', 'test-string-烩€'),
50 50 (u'test-string-烩€', u'test-string-烩€'),
51 51 ])
52 52 def test_string_func_helper(self, raw, expected):
53 53 val = SettingsMaker._string_func(raw)
54 54 assert val == expected
55 55
56 56 @pytest.mark.parametrize('raw, expected', [
57 57 ('', []),
58 58 ('test', ['test']),
59 59 ('CaSe-TeSt', ['CaSe-TeSt']),
60 60 ('test-string-烩€', ['test-string-烩€']),
61 61 (u'test-string-烩€', [u'test-string-烩€']),
62 62 ('hg,git,svn', ['hg', 'git', 'svn']),
63 63 ('hg, git, svn', ['hg', 'git', 'svn']),
64 64
65 65 (', hg , git , svn , ', ['', 'hg', 'git', 'svn', '']),
66 66 ('cheese,free node,other', ['cheese', 'free node', 'other']),
67 67 ], ids=no_newline_id_generator)
68 68 def test_list_setting_helper(self, raw, expected):
69 69 val = SettingsMaker._list_func(raw)
70 70 assert val == expected
71 71
72 72 @pytest.mark.parametrize('raw, expected', [
73 73 ('hg git svn', ['hg', 'git', 'svn']),
74 74 ], ids=no_newline_id_generator)
75 75 def test_list_setting_spaces_helper(self, raw, expected):
76 76 val = SettingsMaker._list_func(raw, sep=' ')
77 77 assert val == expected
78 78
79 79 @pytest.mark.parametrize('raw, expected', [
80 80 ('hg\ngit\nsvn', ['hg', 'git', 'svn']),
81 81 (' hg\n git\n svn ', ['hg', 'git', 'svn']),
82 82 ], ids=no_newline_id_generator)
83 83 def test_list_setting_newlines_helper(self, raw, expected):
84 84 val = SettingsMaker._list_func(raw, sep='\n')
85 85 assert val == expected
86 86
87 87 @pytest.mark.parametrize('raw, expected', [
88 88 ('0', 0),
89 89 ('-0', 0),
90 90 ('12345', 12345),
91 91 ('-12345', -12345),
92 92 (u'-12345', -12345),
93 93 ])
94 94 def test_int_setting_helper(self, raw, expected):
95 95 val = SettingsMaker._int_func(raw)
96 96 assert val == expected
97 97
98 98 @pytest.mark.parametrize('raw', [
99 99 ('0xff'),
100 100 (''),
101 101 ('invalid-int'),
102 102 ('invalid-⁄~†'),
103 103 (u'invalid-⁄~†'),
104 104 ])
105 105 def test_int_setting_helper_invalid_input(self, raw):
106 106 with pytest.raises(Exception):
107 107 SettingsMaker._int_func(raw)
108 108
109 109
110 110 class TestSanitizeVcsSettings(object):
111 111 _bool_funcs = [
112 112 ('vcs.hooks.direct_calls', False),
113 113 ('vcs.server.enable', True),
114 114 ('vcs.start_server', False),
115 115 ('startup.import_repos', False),
116 116 ]
117 117
118 118 _string_funcs = [
119 119 ('vcs.svn.compatible_version', ''),
120 ('vcs.hooks.protocol', 'http'),
120 ('vcs.hooks.protocol.v2', 'celery'),
121 121 ('vcs.hooks.host', '*'),
122 122 ('vcs.scm_app_implementation', 'http'),
123 123 ('vcs.server', ''),
124 124 ('vcs.server.protocol', 'http'),
125 125 ]
126 126
127 127 _list_settings = [
128 128 ('vcs.backends', 'hg git'),
129 129 ]
130 130
131 131 # @pytest.mark.parametrize('key, default', _list_settings)
132 132 # def test_list_setting_spacesep_list(self, key, default):
133 133 # test_list = ['test', 'list', 'values', 'for', key]
134 134 # input_value = ' '.join(test_list)
135 135 # settings = {key: input_value}
136 136 # sanitize_settings_and_apply_defaults({'__file__': ''}, settings)
137 137 # assert settings[key] == test_list
138 138 #
139 139 # @pytest.mark.parametrize('key, default', _list_settings)
140 140 # def test_list_setting_newlinesep_list(self, key, default):
141 141 # test_list = ['test', 'list', 'values', 'for', key]
142 142 # input_value = '\n'.join(test_list)
143 143 # settings = {key: input_value}
144 144 # sanitize_settings_and_apply_defaults({'__file__': ''}, settings)
145 145 # assert settings[key] == test_list
146 146
147 147 @pytest.mark.parametrize('key, default', _list_settings)
148 148 def test_list_setting_commasep_list(self, key, default):
149 149 test_list = ['test', 'list', 'values', 'for', key]
150 150 input_value = ','.join(test_list)
151 151 settings = {key: input_value}
152 152 sanitize_settings_and_apply_defaults({'__file__': ''}, settings)
153 153 assert settings[key] == test_list
154 154
155 155 @pytest.mark.parametrize('key, default', _list_settings)
156 156 def test_list_setting_comma_and_space_sep_list(self, key, default):
157 157 test_list = ['test', 'list', 'values', 'for', key]
158 158 input_value = ', '.join(test_list)
159 159 settings = {key: input_value}
160 160 sanitize_settings_and_apply_defaults({'__file__': ''}, settings)
161 161 assert settings[key] == test_list
162 162
163 163 @pytest.mark.parametrize('key, default', _string_funcs)
164 164 def test_string_func_string(self, key, default):
165 165 test_value = 'test-string-for-{}'.format(key)
166 166 settings = {key: test_value}
167 167 sanitize_settings_and_apply_defaults({'__file__': ''}, settings)
168 168 assert settings[key] == test_value
169 169
170 170 @pytest.mark.parametrize('key, default', _string_funcs)
171 171 def test_string_func_default(self, key, default):
172 172 settings = {}
173 173 sanitize_settings_and_apply_defaults({'__file__': ''}, settings)
174 174 assert settings[key] == default
175 175
176 176 # @pytest.mark.parametrize('key, default', _string_funcs)
177 177 # def test_string_func_lowercase(self, key, default):
178 178 # test_value = 'Test-String-For-{}'.format(key)
179 179 # settings = {key: test_value}
180 180 # sanitize_settings_and_apply_defaults({'__file__': ''}, settings)
181 181 # assert settings[key] == test_value.lower()
182 182
183 183 @pytest.mark.parametrize('key, default', _bool_funcs)
184 184 def test_bool_func_true(self, key, default):
185 185 settings = {key: 'true'}
186 186 sanitize_settings_and_apply_defaults({'__file__': ''}, settings)
187 187 assert settings[key] is True
188 188
189 189 @pytest.mark.parametrize('key, default', _bool_funcs)
190 190 def test_bool_func_false(self, key, default):
191 191 settings = {key: 'false'}
192 192 sanitize_settings_and_apply_defaults({'__file__': ''}, settings)
193 193 assert settings[key] is False
194 194
195 195 @pytest.mark.parametrize('key, default', _bool_funcs)
196 196 def test_bool_func_invalid_string(self, key, default):
197 197 settings = {key: 'no-bool-val-string'}
198 198 sanitize_settings_and_apply_defaults({'__file__': ''}, settings)
199 199 assert settings[key] is False
200 200
201 201 @pytest.mark.parametrize('key, default', _bool_funcs)
202 202 def test_bool_func_default(self, key, default):
203 203 settings = {}
204 204 sanitize_settings_and_apply_defaults({'__file__': ''}, settings)
205 205 assert settings[key] is default
@@ -1,428 +1,428 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 """
21 21 Helpers for fixture generation
22 22 """
23 23
24 24 import os
25 25 import time
26 26 import tempfile
27 27 import shutil
28 28 import configparser
29 29
30 30 from rhodecode.model.settings import SettingsModel
31 31 from rhodecode.model.db import Repository, User, RepoGroup, UserGroup, Gist, UserEmailMap
32 32 from rhodecode.model.meta import Session
33 33 from rhodecode.model.repo import RepoModel
34 34 from rhodecode.model.user import UserModel
35 35 from rhodecode.model.repo_group import RepoGroupModel
36 36 from rhodecode.model.user_group import UserGroupModel
37 37 from rhodecode.model.gist import GistModel
38 38 from rhodecode.model.auth_token import AuthTokenModel
39 39 from rhodecode.model.scm import ScmModel
40 40 from rhodecode.authentication.plugins.auth_rhodecode import \
41 41 RhodeCodeAuthPlugin
42 42
43 43 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
44 44
45 45 dn = os.path.dirname
46 46 FIXTURES = os.path.join(dn(dn(os.path.abspath(__file__))), 'tests', 'fixtures')
47 47
48 48
49 49 def error_function(*args, **kwargs):
50 50 raise Exception('Total Crash !')
51 51
52 52
53 53 class TestINI(object):
54 54 """
55 55 Allows to create a new test.ini file as a copy of existing one with edited
56 56 data. Example usage::
57 57
58 58 with TestINI('test.ini', [{'section':{'key':val'}]) as new_test_ini_path:
59 59 print('paster server %s' % new_test_ini)
60 60 """
61 61
62 62 def __init__(self, ini_file_path, ini_params, new_file_prefix='DEFAULT',
63 63 destroy=True, dir=None):
64 64 self.ini_file_path = ini_file_path
65 65 self.ini_params = ini_params
66 66 self.new_path = None
67 67 self.new_path_prefix = new_file_prefix
68 68 self._destroy = destroy
69 69 self._dir = dir
70 70
71 71 def __enter__(self):
72 72 return self.create()
73 73
74 74 def __exit__(self, exc_type, exc_val, exc_tb):
75 75 self.destroy()
76 76
77 77 def create(self):
78 78 parser = configparser.ConfigParser()
79 79 parser.read(self.ini_file_path)
80 80
81 81 for data in self.ini_params:
82 82 section, ini_params = list(data.items())[0]
83 83
84 84 for key, val in ini_params.items():
85 85 parser[section][key] = str(val)
86 86
87 87 with tempfile.NamedTemporaryFile(
88 88 mode='w',
89 89 prefix=self.new_path_prefix, suffix='.ini', dir=self._dir,
90 90 delete=False) as new_ini_file:
91 91 parser.write(new_ini_file)
92 92 self.new_path = new_ini_file.name
93 93
94 94 return self.new_path
95 95
96 96 def destroy(self):
97 97 if self._destroy:
98 98 os.remove(self.new_path)
99 99
100 100
101 101 class Fixture(object):
102 102
103 103 def anon_access(self, status):
104 104 """
105 105 Context process for disabling anonymous access. use like:
106 106 fixture = Fixture()
107 107 with fixture.anon_access(False):
108 108 #tests
109 109
110 110 after this block anon access will be set to `not status`
111 111 """
112 112
113 113 class context(object):
114 114 def __enter__(self):
115 115 anon = User.get_default_user()
116 116 anon.active = status
117 117 Session().add(anon)
118 118 Session().commit()
119 119 time.sleep(1.5) # must sleep for cache (1s to expire)
120 120
121 121 def __exit__(self, exc_type, exc_val, exc_tb):
122 122 anon = User.get_default_user()
123 123 anon.active = not status
124 124 Session().add(anon)
125 125 Session().commit()
126 126
127 127 return context()
128 128
129 129 def auth_restriction(self, registry, auth_restriction):
130 130 """
131 131 Context process for changing the builtin rhodecode plugin auth restrictions.
132 132 Use like:
133 133 fixture = Fixture()
134 134 with fixture.auth_restriction('super_admin'):
135 135 #tests
136 136
137 137 after this block auth restriction will be taken off
138 138 """
139 139
140 140 class context(object):
141 141 def _get_plugin(self):
142 142 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
143 143 plugin = RhodeCodeAuthPlugin(plugin_id)
144 144 return plugin
145 145
146 146 def __enter__(self):
147 147
148 148 plugin = self._get_plugin()
149 149 plugin.create_or_update_setting('auth_restriction', auth_restriction)
150 150 Session().commit()
151 151 SettingsModel().invalidate_settings_cache(hard=True)
152 152
153 153 def __exit__(self, exc_type, exc_val, exc_tb):
154 154
155 155 plugin = self._get_plugin()
156 156 plugin.create_or_update_setting(
157 157 'auth_restriction', RhodeCodeAuthPlugin.AUTH_RESTRICTION_NONE)
158 158 Session().commit()
159 159 SettingsModel().invalidate_settings_cache(hard=True)
160 160
161 161 return context()
162 162
163 163 def scope_restriction(self, registry, scope_restriction):
164 164 """
165 165 Context process for changing the builtin rhodecode plugin scope restrictions.
166 166 Use like:
167 167 fixture = Fixture()
168 168 with fixture.scope_restriction('scope_http'):
169 169 #tests
170 170
171 171 after this block scope restriction will be taken off
172 172 """
173 173
174 174 class context(object):
175 175 def _get_plugin(self):
176 176 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
177 177 plugin = RhodeCodeAuthPlugin(plugin_id)
178 178 return plugin
179 179
180 180 def __enter__(self):
181 181 plugin = self._get_plugin()
182 182 plugin.create_or_update_setting('scope_restriction', scope_restriction)
183 183 Session().commit()
184 184 SettingsModel().invalidate_settings_cache(hard=True)
185 185
186 186 def __exit__(self, exc_type, exc_val, exc_tb):
187 187 plugin = self._get_plugin()
188 188 plugin.create_or_update_setting(
189 189 'scope_restriction', RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_ALL)
190 190 Session().commit()
191 191 SettingsModel().invalidate_settings_cache(hard=True)
192 192
193 193 return context()
194 194
195 195 def _get_repo_create_params(self, **custom):
196 196 repo_type = custom.get('repo_type') or 'hg'
197 197
198 198 default_landing_ref, landing_ref_lbl = ScmModel.backend_landing_ref(repo_type)
199 199
200 200 defs = {
201 201 'repo_name': None,
202 202 'repo_type': repo_type,
203 203 'clone_uri': '',
204 204 'push_uri': '',
205 205 'repo_group': '-1',
206 206 'repo_description': 'DESC',
207 207 'repo_private': False,
208 208 'repo_landing_commit_ref': default_landing_ref,
209 209 'repo_copy_permissions': False,
210 210 'repo_state': Repository.STATE_CREATED,
211 211 }
212 212 defs.update(custom)
213 213 if 'repo_name_full' not in custom:
214 214 defs.update({'repo_name_full': defs['repo_name']})
215 215
216 216 # fix the repo name if passed as repo_name_full
217 217 if defs['repo_name']:
218 218 defs['repo_name'] = defs['repo_name'].split('/')[-1]
219 219
220 220 return defs
221 221
222 222 def _get_group_create_params(self, **custom):
223 223 defs = {
224 224 'group_name': None,
225 225 'group_description': 'DESC',
226 226 'perm_updates': [],
227 227 'perm_additions': [],
228 228 'perm_deletions': [],
229 229 'group_parent_id': -1,
230 230 'enable_locking': False,
231 231 'recursive': False,
232 232 }
233 233 defs.update(custom)
234 234
235 235 return defs
236 236
237 237 def _get_user_create_params(self, name, **custom):
238 238 defs = {
239 239 'username': name,
240 240 'password': 'qweqwe',
241 241 'email': '%s+test@rhodecode.org' % name,
242 242 'firstname': 'TestUser',
243 243 'lastname': 'Test',
244 244 'description': 'test description',
245 245 'active': True,
246 246 'admin': False,
247 247 'extern_type': 'rhodecode',
248 248 'extern_name': None,
249 249 }
250 250 defs.update(custom)
251 251
252 252 return defs
253 253
254 254 def _get_user_group_create_params(self, name, **custom):
255 255 defs = {
256 256 'users_group_name': name,
257 257 'user_group_description': 'DESC',
258 258 'users_group_active': True,
259 259 'user_group_data': {},
260 260 }
261 261 defs.update(custom)
262 262
263 263 return defs
264 264
265 265 def create_repo(self, name, **kwargs):
266 266 repo_group = kwargs.get('repo_group')
267 267 if isinstance(repo_group, RepoGroup):
268 268 kwargs['repo_group'] = repo_group.group_id
269 269 name = name.split(Repository.NAME_SEP)[-1]
270 270 name = Repository.NAME_SEP.join((repo_group.group_name, name))
271 271
272 272 if 'skip_if_exists' in kwargs:
273 273 del kwargs['skip_if_exists']
274 274 r = Repository.get_by_repo_name(name)
275 275 if r:
276 276 return r
277 277
278 278 form_data = self._get_repo_create_params(repo_name=name, **kwargs)
279 279 cur_user = kwargs.get('cur_user', TEST_USER_ADMIN_LOGIN)
280 280 RepoModel().create(form_data, cur_user)
281 281 Session().commit()
282 282 repo = Repository.get_by_repo_name(name)
283 283 assert repo
284 284 return repo
285 285
286 286 def create_fork(self, repo_to_fork, fork_name, **kwargs):
287 287 repo_to_fork = Repository.get_by_repo_name(repo_to_fork)
288 288
289 289 form_data = self._get_repo_create_params(
290 290 repo_name=fork_name,
291 291 fork_parent_id=repo_to_fork.repo_id,
292 292 repo_type=repo_to_fork.repo_type,
293 293 **kwargs)
294 294
295 295 # TODO: fix it !!
296 296 form_data['description'] = form_data['repo_description']
297 297 form_data['private'] = form_data['repo_private']
298 298 form_data['landing_rev'] = form_data['repo_landing_commit_ref']
299 299
300 300 owner = kwargs.get('cur_user', TEST_USER_ADMIN_LOGIN)
301 301 RepoModel().create_fork(form_data, cur_user=owner)
302 302 Session().commit()
303 303 r = Repository.get_by_repo_name(fork_name)
304 304 assert r
305 305 return r
306 306
307 307 def destroy_repo(self, repo_name, **kwargs):
308 RepoModel().delete(repo_name, pull_requests='delete', **kwargs)
308 RepoModel().delete(repo_name, pull_requests='delete', artifacts='delete', **kwargs)
309 309 Session().commit()
310 310
311 311 def destroy_repo_on_filesystem(self, repo_name):
312 312 rm_path = os.path.join(RepoModel().repos_path, repo_name)
313 313 if os.path.isdir(rm_path):
314 314 shutil.rmtree(rm_path)
315 315
316 316 def create_repo_group(self, name, **kwargs):
317 317 if 'skip_if_exists' in kwargs:
318 318 del kwargs['skip_if_exists']
319 319 gr = RepoGroup.get_by_group_name(group_name=name)
320 320 if gr:
321 321 return gr
322 322 form_data = self._get_group_create_params(group_name=name, **kwargs)
323 323 owner = kwargs.get('cur_user', TEST_USER_ADMIN_LOGIN)
324 324 gr = RepoGroupModel().create(
325 325 group_name=form_data['group_name'],
326 326 group_description=form_data['group_name'],
327 327 owner=owner)
328 328 Session().commit()
329 329 gr = RepoGroup.get_by_group_name(gr.group_name)
330 330 return gr
331 331
332 332 def destroy_repo_group(self, repogroupid):
333 333 RepoGroupModel().delete(repogroupid)
334 334 Session().commit()
335 335
336 336 def create_user(self, name, **kwargs):
337 337 if 'skip_if_exists' in kwargs:
338 338 del kwargs['skip_if_exists']
339 339 user = User.get_by_username(name)
340 340 if user:
341 341 return user
342 342 form_data = self._get_user_create_params(name, **kwargs)
343 343 user = UserModel().create(form_data)
344 344
345 345 # create token for user
346 346 AuthTokenModel().create(
347 347 user=user, description=u'TEST_USER_TOKEN')
348 348
349 349 Session().commit()
350 350 user = User.get_by_username(user.username)
351 351 return user
352 352
353 353 def destroy_user(self, userid):
354 354 UserModel().delete(userid)
355 355 Session().commit()
356 356
357 357 def create_additional_user_email(self, user, email):
358 358 uem = UserEmailMap()
359 359 uem.user = user
360 360 uem.email = email
361 361 Session().add(uem)
362 362 return uem
363 363
364 364 def destroy_users(self, userid_iter):
365 365 for user_id in userid_iter:
366 366 if User.get_by_username(user_id):
367 367 UserModel().delete(user_id)
368 368 Session().commit()
369 369
370 370 def create_user_group(self, name, **kwargs):
371 371 if 'skip_if_exists' in kwargs:
372 372 del kwargs['skip_if_exists']
373 373 gr = UserGroup.get_by_group_name(group_name=name)
374 374 if gr:
375 375 return gr
376 376 # map active flag to the real attribute. For API consistency of fixtures
377 377 if 'active' in kwargs:
378 378 kwargs['users_group_active'] = kwargs['active']
379 379 del kwargs['active']
380 380 form_data = self._get_user_group_create_params(name, **kwargs)
381 381 owner = kwargs.get('cur_user', TEST_USER_ADMIN_LOGIN)
382 382 user_group = UserGroupModel().create(
383 383 name=form_data['users_group_name'],
384 384 description=form_data['user_group_description'],
385 385 owner=owner, active=form_data['users_group_active'],
386 386 group_data=form_data['user_group_data'])
387 387 Session().commit()
388 388 user_group = UserGroup.get_by_group_name(user_group.users_group_name)
389 389 return user_group
390 390
391 391 def destroy_user_group(self, usergroupid):
392 392 UserGroupModel().delete(user_group=usergroupid, force=True)
393 393 Session().commit()
394 394
395 395 def create_gist(self, **kwargs):
396 396 form_data = {
397 397 'description': 'new-gist',
398 398 'owner': TEST_USER_ADMIN_LOGIN,
399 399 'gist_type': GistModel.cls.GIST_PUBLIC,
400 400 'lifetime': -1,
401 401 'acl_level': Gist.ACL_LEVEL_PUBLIC,
402 402 'gist_mapping': {b'filename1.txt': {'content': b'hello world'},}
403 403 }
404 404 form_data.update(kwargs)
405 405 gist = GistModel().create(
406 406 description=form_data['description'], owner=form_data['owner'],
407 407 gist_mapping=form_data['gist_mapping'], gist_type=form_data['gist_type'],
408 408 lifetime=form_data['lifetime'], gist_acl_level=form_data['acl_level']
409 409 )
410 410 Session().commit()
411 411 return gist
412 412
413 413 def destroy_gists(self, gistid=None):
414 414 for g in GistModel.cls.get_all():
415 415 if gistid:
416 416 if gistid == g.gist_access_id:
417 417 GistModel().delete(g)
418 418 else:
419 419 GistModel().delete(g)
420 420 Session().commit()
421 421
422 422 def load_resource(self, resource_name, strip=False):
423 423 with open(os.path.join(FIXTURES, resource_name), 'rb') as f:
424 424 source = f.read()
425 425 if strip:
426 426 source = source.strip()
427 427
428 428 return source
@@ -1,226 +1,226 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21
22 22 from rhodecode.lib.config_utils import get_app_config
23 23 from rhodecode.tests.fixture import TestINI
24 24 from rhodecode.tests import TESTS_TMP_PATH
25 25 from rhodecode.tests.server_utils import RcVCSServer
26 26
27 27
28 28 @pytest.fixture(scope='session')
29 29 def vcsserver(request, vcsserver_port, vcsserver_factory):
30 30 """
31 31 Session scope VCSServer.
32 32
33 33 Tests which need the VCSServer have to rely on this fixture in order
34 34 to ensure it will be running.
35 35
36 36 For specific needs, the fixture vcsserver_factory can be used. It allows to
37 37 adjust the configuration file for the test run.
38 38
39 39 Command line args:
40 40
41 41 --without-vcsserver: Allows to switch this fixture off. You have to
42 42 manually start the server.
43 43
44 44 --vcsserver-port: Will expect the VCSServer to listen on this port.
45 45 """
46 46
47 47 if not request.config.getoption('with_vcsserver'):
48 48 return None
49 49
50 50 return vcsserver_factory(
51 51 request, vcsserver_port=vcsserver_port)
52 52
53 53
54 54 @pytest.fixture(scope='session')
55 55 def vcsserver_factory(tmpdir_factory):
56 56 """
57 57 Use this if you need a running vcsserver with a special configuration.
58 58 """
59 59
60 60 def factory(request, overrides=(), vcsserver_port=None,
61 61 log_file=None, workers='3'):
62 62
63 63 if vcsserver_port is None:
64 64 vcsserver_port = get_available_port()
65 65
66 66 overrides = list(overrides)
67 67 overrides.append({'server:main': {'port': vcsserver_port}})
68 68
69 69 option_name = 'vcsserver_config_http'
70 70 override_option_name = 'vcsserver_config_override'
71 71 config_file = get_config(
72 72 request.config, option_name=option_name,
73 73 override_option_name=override_option_name, overrides=overrides,
74 74 basetemp=tmpdir_factory.getbasetemp().strpath,
75 75 prefix='test_vcs_')
76 76
77 77 server = RcVCSServer(config_file, log_file, workers)
78 78 server.start()
79 79
80 80 @request.addfinalizer
81 81 def cleanup():
82 82 server.shutdown()
83 83
84 84 server.wait_until_ready()
85 85 return server
86 86
87 87 return factory
88 88
89 89
90 90 def _use_log_level(config):
91 91 level = config.getoption('test_loglevel') or 'critical'
92 92 return level.upper()
93 93
94 94
95 95 @pytest.fixture(scope='session')
96 96 def ini_config(request, tmpdir_factory, rcserver_port, vcsserver_port):
97 97 option_name = 'pyramid_config'
98 98 log_level = _use_log_level(request.config)
99 99
100 100 overrides = [
101 101 {'server:main': {'port': rcserver_port}},
102 102 {'app:main': {
103 103 'cache_dir': '%(here)s/rc-tests/rc_data',
104 104 'vcs.server': f'localhost:{vcsserver_port}',
105 105 # johbo: We will always start the VCSServer on our own based on the
106 106 # fixtures of the test cases. For the test run it must always be
107 107 # off in the INI file.
108 108 'vcs.start_server': 'false',
109 109
110 110 'vcs.server.protocol': 'http',
111 111 'vcs.scm_app_implementation': 'http',
112 112 'vcs.svn.proxy.enabled': 'true',
113 'vcs.hooks.protocol': 'http',
113 'vcs.hooks.protocol.v2': 'celery',
114 114 'vcs.hooks.host': '*',
115 115 'repo_store.path': TESTS_TMP_PATH,
116 116 'app.service_api.token': 'service_secret_token',
117 117 }},
118 118
119 119 {'handler_console': {
120 120 'class': 'StreamHandler',
121 121 'args': '(sys.stderr,)',
122 122 'level': log_level,
123 123 }},
124 124
125 125 ]
126 126
127 127 filename = get_config(
128 128 request.config, option_name=option_name,
129 129 override_option_name='{}_override'.format(option_name),
130 130 overrides=overrides,
131 131 basetemp=tmpdir_factory.getbasetemp().strpath,
132 132 prefix='test_rce_')
133 133 return filename
134 134
135 135
136 136 @pytest.fixture(scope='session')
137 137 def ini_settings(ini_config):
138 138 ini_path = ini_config
139 139 return get_app_config(ini_path)
140 140
141 141
142 142 def get_available_port(min_port=40000, max_port=55555):
143 143 from rhodecode.lib.utils2 import get_available_port as _get_port
144 144 return _get_port(min_port, max_port)
145 145
146 146
147 147 @pytest.fixture(scope='session')
148 148 def rcserver_port(request):
149 149 port = get_available_port()
150 150 print(f'Using rhodecode port {port}')
151 151 return port
152 152
153 153
154 154 @pytest.fixture(scope='session')
155 155 def vcsserver_port(request):
156 156 port = request.config.getoption('--vcsserver-port')
157 157 if port is None:
158 158 port = get_available_port()
159 159 print(f'Using vcsserver port {port}')
160 160 return port
161 161
162 162
163 163 @pytest.fixture(scope='session')
164 164 def available_port_factory() -> get_available_port:
165 165 """
166 166 Returns a callable which returns free port numbers.
167 167 """
168 168 return get_available_port
169 169
170 170
171 171 @pytest.fixture()
172 172 def available_port(available_port_factory):
173 173 """
174 174 Gives you one free port for the current test.
175 175
176 176 Uses "available_port_factory" to retrieve the port.
177 177 """
178 178 return available_port_factory()
179 179
180 180
181 181 @pytest.fixture(scope='session')
182 182 def testini_factory(tmpdir_factory, ini_config):
183 183 """
184 184 Factory to create an INI file based on TestINI.
185 185
186 186 It will make sure to place the INI file in the correct directory.
187 187 """
188 188 basetemp = tmpdir_factory.getbasetemp().strpath
189 189 return TestIniFactory(basetemp, ini_config)
190 190
191 191
192 192 class TestIniFactory(object):
193 193
194 194 def __init__(self, basetemp, template_ini):
195 195 self._basetemp = basetemp
196 196 self._template_ini = template_ini
197 197
198 198 def __call__(self, ini_params, new_file_prefix='test'):
199 199 ini_file = TestINI(
200 200 self._template_ini, ini_params=ini_params,
201 201 new_file_prefix=new_file_prefix, dir=self._basetemp)
202 202 result = ini_file.create()
203 203 return result
204 204
205 205
206 206 def get_config(
207 207 config, option_name, override_option_name, overrides=None,
208 208 basetemp=None, prefix='test'):
209 209 """
210 210 Find a configuration file and apply overrides for the given `prefix`.
211 211 """
212 212 config_file = (
213 213 config.getoption(option_name) or config.getini(option_name))
214 214 if not config_file:
215 215 pytest.exit(
216 216 "Configuration error, could not extract {}.".format(option_name))
217 217
218 218 overrides = overrides or []
219 219 config_override = config.getoption(override_option_name)
220 220 if config_override:
221 221 overrides.append(config_override)
222 222 temp_ini_file = TestINI(
223 223 config_file, ini_params=overrides, new_file_prefix=prefix,
224 224 dir=basetemp)
225 225
226 226 return temp_ini_file.create()
@@ -1,155 +1,154 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import urllib.parse
21 21
22 22 import mock
23 23 import pytest
24 24 import simplejson as json
25 25
26 26 from rhodecode.lib.vcs.backends.base import Config
27 27 from rhodecode.tests.lib.middleware import mock_scm_app
28 28 import rhodecode.lib.middleware.simplehg as simplehg
29 29
30 30
31 31 def get_environ(url):
32 32 """Construct a minimum WSGI environ based on the URL."""
33 33 parsed_url = urllib.parse.urlparse(url)
34 34 environ = {
35 35 'PATH_INFO': parsed_url.path,
36 36 'QUERY_STRING': parsed_url.query,
37 37 }
38 38
39 39 return environ
40 40
41 41
42 42 @pytest.mark.parametrize(
43 43 'url, expected_action',
44 44 [
45 45 ('/foo/bar?cmd=unbundle&key=tip', 'push'),
46 46 ('/foo/bar?cmd=pushkey&key=tip', 'push'),
47 47 ('/foo/bar?cmd=listkeys&key=tip', 'pull'),
48 48 ('/foo/bar?cmd=changegroup&key=tip', 'pull'),
49 49 ('/foo/bar?cmd=hello', 'pull'),
50 50 ('/foo/bar?cmd=batch', 'push'),
51 51 ('/foo/bar?cmd=putlfile', 'push'),
52 52 # Edge case: unknown argument: assume push
53 53 ('/foo/bar?cmd=unknown&key=tip', 'push'),
54 54 ('/foo/bar?cmd=&key=tip', 'push'),
55 55 # Edge case: not cmd argument
56 56 ('/foo/bar?key=tip', 'push'),
57 57 ])
58 58 def test_get_action(url, expected_action, request_stub):
59 59 app = simplehg.SimpleHg(config={'auth_ret_code': '', 'base_path': ''},
60 60 registry=request_stub.registry)
61 61 assert expected_action == app._get_action(get_environ(url))
62 62
63 63
64 64 @pytest.mark.parametrize(
65 65 'environ, expected_xargs, expected_batch',
66 66 [
67 67 ({},
68 68 [''], ['push']),
69 69
70 70 ({'HTTP_X_HGARG_1': ''},
71 71 [''], ['push']),
72 72
73 73 ({'HTTP_X_HGARG_1': 'cmds=listkeys+namespace%3Dphases'},
74 74 ['listkeys namespace=phases'], ['pull']),
75 75
76 76 ({'HTTP_X_HGARG_1': 'cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'},
77 77 ['pushkey namespace=bookmarks,key=bm,old=,new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'], ['push']),
78 78
79 79 ({'HTTP_X_HGARG_1': 'namespace=phases'},
80 80 ['namespace=phases'], ['push']),
81 81
82 82 ])
83 83 def test_xarg_and_batch_commands(environ, expected_xargs, expected_batch):
84 84 app = simplehg.SimpleHg
85 85
86 86 result = app._get_xarg_headers(environ)
87 87 result_batch = app._get_batch_cmd(environ)
88 88 assert expected_xargs == result
89 89 assert expected_batch == result_batch
90 90
91 91
92 92 @pytest.mark.parametrize(
93 93 'url, expected_repo_name',
94 94 [
95 95 ('/foo?cmd=unbundle&key=tip', 'foo'),
96 96 ('/foo/bar?cmd=pushkey&key=tip', 'foo/bar'),
97 97 ('/foo/bar/baz?cmd=listkeys&key=tip', 'foo/bar/baz'),
98 98 # Repos with trailing slashes.
99 99 ('/foo/?cmd=unbundle&key=tip', 'foo'),
100 100 ('/foo/bar/?cmd=pushkey&key=tip', 'foo/bar'),
101 101 ('/foo/bar/baz/?cmd=listkeys&key=tip', 'foo/bar/baz'),
102 102 ])
103 103 def test_get_repository_name(url, expected_repo_name, request_stub):
104 104 app = simplehg.SimpleHg(config={'auth_ret_code': '', 'base_path': ''},
105 105 registry=request_stub.registry)
106 106 assert expected_repo_name == app._get_repository_name(get_environ(url))
107 107
108 108
109 109 def test_get_config(user_util, baseapp, request_stub):
110 110 repo = user_util.create_repo(repo_type='git')
111 111 app = simplehg.SimpleHg(config={'auth_ret_code': '', 'base_path': ''},
112 112 registry=request_stub.registry)
113 113 extras = [('foo', 'FOO', 'bar', 'BAR')]
114 114
115 115 hg_config = app._create_config(extras, repo_name=repo.repo_name)
116 116
117 117 config = simplehg.utils.make_db_config(repo=repo.repo_name)
118 118 config.set('rhodecode', 'RC_SCM_DATA', json.dumps(extras))
119 119 hg_config_org = config
120 120
121 121 expected_config = [
122 122 ('vcs_svn_tag', 'ff89f8c714d135d865f44b90e5413b88de19a55f', '/tags/*'),
123 ('web', 'push_ssl', 'False'),
124 123 ('web', 'allow_push', '*'),
125 124 ('web', 'allow_archive', 'gz zip bz2'),
126 125 ('web', 'baseurl', '/'),
127 126 ('vcs_git_lfs', 'store_location', hg_config_org.get('vcs_git_lfs', 'store_location')),
128 127 ('vcs_svn_branch', '9aac1a38c3b8a0cdc4ae0f960a5f83332bc4fa5e', '/branches/*'),
129 128 ('vcs_svn_branch', 'c7e6a611c87da06529fd0dd733308481d67c71a8', '/trunk'),
130 129 ('largefiles', 'usercache', hg_config_org.get('largefiles', 'usercache')),
131 130 ('hooks', 'preoutgoing.pre_pull', 'python:vcsserver.hooks.pre_pull'),
132 131 ('hooks', 'prechangegroup.pre_push', 'python:vcsserver.hooks.pre_push'),
133 132 ('hooks', 'outgoing.pull_logger', 'python:vcsserver.hooks.log_pull_action'),
134 133 ('hooks', 'pretxnchangegroup.pre_push', 'python:vcsserver.hooks.pre_push'),
135 134 ('hooks', 'changegroup.push_logger', 'python:vcsserver.hooks.log_push_action'),
136 135 ('hooks', 'changegroup.repo_size', 'python:vcsserver.hooks.repo_size'),
137 136 ('phases', 'publish', 'True'),
138 137 ('extensions', 'largefiles', ''),
139 138 ('paths', '/', hg_config_org.get('paths', '/')),
140 139 ('rhodecode', 'RC_SCM_DATA', '[["foo","FOO","bar","BAR"]]')
141 140 ]
142 141 for entry in expected_config:
143 142 assert entry in hg_config
144 143
145 144
146 145 def test_create_wsgi_app_uses_scm_app_from_simplevcs(request_stub):
147 146 config = {
148 147 'auth_ret_code': '',
149 148 'base_path': '',
150 149 'vcs.scm_app_implementation':
151 150 'rhodecode.tests.lib.middleware.mock_scm_app',
152 151 }
153 152 app = simplehg.SimpleHg(config=config, registry=request_stub.registry)
154 153 wsgi_app = app._create_wsgi_app('/tmp/test', 'test_repo', {})
155 154 assert wsgi_app is mock_scm_app.mock_hg_wsgi
@@ -1,451 +1,448 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import mock
21 21 import pytest
22 22
23 23 from rhodecode.lib.str_utils import base64_to_str
24 24 from rhodecode.lib.utils2 import AttributeDict
25 25 from rhodecode.tests.utils import CustomTestApp
26 26
27 27 from rhodecode.lib.caching_query import FromCache
28 28 from rhodecode.lib.middleware import simplevcs
29 29 from rhodecode.lib.middleware.https_fixup import HttpsFixup
30 30 from rhodecode.lib.middleware.utils import scm_app_http
31 31 from rhodecode.model.db import User, _hash_key
32 32 from rhodecode.model.meta import Session, cache as db_cache
33 33 from rhodecode.tests import (
34 34 HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
35 35 from rhodecode.tests.lib.middleware import mock_scm_app
36 36
37 37
38 38 class StubVCSController(simplevcs.SimpleVCS):
39 39
40 40 SCM = 'hg'
41 41 stub_response_body = tuple()
42 42
43 43 def __init__(self, *args, **kwargs):
44 44 super(StubVCSController, self).__init__(*args, **kwargs)
45 45 self._action = 'pull'
46 46 self._is_shadow_repo_dir = True
47 47 self._name = HG_REPO
48 48 self.set_repo_names(None)
49 49
50 50 @property
51 51 def is_shadow_repo_dir(self):
52 52 return self._is_shadow_repo_dir
53 53
54 54 def _get_repository_name(self, environ):
55 55 return self._name
56 56
57 57 def _get_action(self, environ):
58 58 return self._action
59 59
60 60 def _create_wsgi_app(self, repo_path, repo_name, config):
61 61 def fake_app(environ, start_response):
62 62 headers = [
63 63 ('Http-Accept', 'application/mercurial')
64 64 ]
65 65 start_response('200 OK', headers)
66 66 return self.stub_response_body
67 67 return fake_app
68 68
69 69 def _create_config(self, extras, repo_name, scheme='http'):
70 70 return None
71 71
72 72
73 73 @pytest.fixture()
74 74 def vcscontroller(baseapp, config_stub, request_stub):
75 75 from rhodecode.config.middleware import ce_auth_resources
76 76
77 77 config_stub.testing_securitypolicy()
78 78 config_stub.include('rhodecode.authentication')
79 79
80 80 for resource in ce_auth_resources:
81 81 config_stub.include(resource)
82 82
83 83 controller = StubVCSController(
84 84 baseapp.config.get_settings(), request_stub.registry)
85 85 app = HttpsFixup(controller, baseapp.config.get_settings())
86 86 app = CustomTestApp(app)
87 87
88 88 _remove_default_user_from_query_cache()
89 89
90 90 # Sanity checks that things are set up correctly
91 91 app.get('/' + HG_REPO, status=200)
92 92
93 93 app.controller = controller
94 94 return app
95 95
96 96
97 97 def _remove_default_user_from_query_cache():
98 98 user = User.get_default_user(cache=True)
99 99 query = Session().query(User).filter(User.username == user.username)
100 100 query = query.options(
101 101 FromCache("sql_cache_short", f"get_user_{_hash_key(user.username)}"))
102 102
103 103 db_cache.invalidate(
104 104 query, {},
105 105 FromCache("sql_cache_short", f"get_user_{_hash_key(user.username)}"))
106 106
107 107 Session().expire(user)
108 108
109 109
110 110 def test_handles_exceptions_during_permissions_checks(
111 111 vcscontroller, disable_anonymous_user, enable_auth_plugins, test_user_factory):
112 112
113 113 test_password = 'qweqwe'
114 114 test_user = test_user_factory(password=test_password, extern_type='headers', extern_name='headers')
115 115 test_username = test_user.username
116 116
117 117 enable_auth_plugins.enable([
118 118 'egg:rhodecode-enterprise-ce#headers',
119 119 'egg:rhodecode-enterprise-ce#token',
120 120 'egg:rhodecode-enterprise-ce#rhodecode'],
121 121 override={
122 122 'egg:rhodecode-enterprise-ce#headers': {'auth_headers_header': 'REMOTE_USER'}
123 123 })
124 124
125 125 user_and_pass = f'{test_username}:{test_password}'
126 126 auth_password = base64_to_str(user_and_pass)
127 127
128 128 extra_environ = {
129 129 'AUTH_TYPE': 'Basic',
130 130 'HTTP_AUTHORIZATION': f'Basic {auth_password}',
131 131 'REMOTE_USER': test_username,
132 132 }
133 133
134 134 # Verify that things are hooked up correctly, we pass user with headers bound auth, and headers filled in
135 135 vcscontroller.get('/', status=200, extra_environ=extra_environ)
136 136
137 137 # Simulate trouble during permission checks
138 138 with mock.patch('rhodecode.model.db.User.get_by_username',
139 139 side_effect=Exception('permission_error_test')) as get_user:
140 140 # Verify that a correct 500 is returned and check that the expected
141 141 # code path was hit.
142 142 vcscontroller.get('/', status=500, extra_environ=extra_environ)
143 143 assert get_user.called
144 144
145 145
146 146 class StubFailVCSController(simplevcs.SimpleVCS):
147 147 def _handle_request(self, environ, start_response):
148 148 raise Exception("BOOM")
149 149
150 150
151 151 @pytest.fixture(scope='module')
152 152 def fail_controller(baseapp):
153 153 controller = StubFailVCSController(
154 154 baseapp.config.get_settings(), baseapp.config)
155 155 controller = HttpsFixup(controller, baseapp.config.get_settings())
156 156 controller = CustomTestApp(controller)
157 157 return controller
158 158
159 159
160 160 def test_handles_exceptions_as_internal_server_error(fail_controller):
161 161 fail_controller.get('/', status=500)
162 162
163 163
164 164 def test_provides_traceback_for_appenlight(fail_controller):
165 165 response = fail_controller.get(
166 166 '/', status=500, extra_environ={'appenlight.client': 'fake'})
167 167 assert 'appenlight.__traceback' in response.request.environ
168 168
169 169
170 170 def test_provides_utils_scm_app_as_scm_app_by_default(baseapp, request_stub):
171 171 controller = StubVCSController(baseapp.config.get_settings(), request_stub.registry)
172 172 assert controller.scm_app is scm_app_http
173 173
174 174
175 175 def test_allows_to_override_scm_app_via_config(baseapp, request_stub):
176 176 config = baseapp.config.get_settings().copy()
177 177 config['vcs.scm_app_implementation'] = (
178 178 'rhodecode.tests.lib.middleware.mock_scm_app')
179 179 controller = StubVCSController(config, request_stub.registry)
180 180 assert controller.scm_app is mock_scm_app
181 181
182 182
183 183 @pytest.mark.parametrize('query_string, expected', [
184 184 ('cmd=stub_command', True),
185 185 ('cmd=listkeys', False),
186 186 ])
187 187 def test_should_check_locking(query_string, expected):
188 188 result = simplevcs._should_check_locking(query_string)
189 189 assert result == expected
190 190
191 191
192 192 class TestShadowRepoRegularExpression(object):
193 193 pr_segment = 'pull-request'
194 194 shadow_segment = 'repository'
195 195
196 196 @pytest.mark.parametrize('url, expected', [
197 197 # repo with/without groups
198 198 ('My-Repo/{pr_segment}/1/{shadow_segment}', True),
199 199 ('Group/My-Repo/{pr_segment}/2/{shadow_segment}', True),
200 200 ('Group/Sub-Group/My-Repo/{pr_segment}/3/{shadow_segment}', True),
201 201 ('Group/Sub-Group1/Sub-Group2/My-Repo/{pr_segment}/3/{shadow_segment}', True),
202 202
203 203 # pull request ID
204 204 ('MyRepo/{pr_segment}/1/{shadow_segment}', True),
205 205 ('MyRepo/{pr_segment}/1234567890/{shadow_segment}', True),
206 206 ('MyRepo/{pr_segment}/-1/{shadow_segment}', False),
207 207 ('MyRepo/{pr_segment}/invalid/{shadow_segment}', False),
208 208
209 209 # unicode
210 210 (u'Sp€çîál-Repö/{pr_segment}/1/{shadow_segment}', True),
211 211 (u'Sp€çîál-Gröüp/Sp€çîál-Repö/{pr_segment}/1/{shadow_segment}', True),
212 212
213 213 # trailing/leading slash
214 214 ('/My-Repo/{pr_segment}/1/{shadow_segment}', False),
215 215 ('My-Repo/{pr_segment}/1/{shadow_segment}/', False),
216 216 ('/My-Repo/{pr_segment}/1/{shadow_segment}/', False),
217 217
218 218 # misc
219 219 ('My-Repo/{pr_segment}/1/{shadow_segment}/extra', False),
220 220 ('My-Repo/{pr_segment}/1/{shadow_segment}extra', False),
221 221 ])
222 222 def test_shadow_repo_regular_expression(self, url, expected):
223 223 from rhodecode.lib.middleware.simplevcs import SimpleVCS
224 224 url = url.format(
225 225 pr_segment=self.pr_segment,
226 226 shadow_segment=self.shadow_segment)
227 227 match_obj = SimpleVCS.shadow_repo_re.match(url)
228 228 assert (match_obj is not None) == expected
229 229
230 230
231 231 @pytest.mark.backends('git', 'hg')
232 232 class TestShadowRepoExposure(object):
233 233
234 234 def test_pull_on_shadow_repo_propagates_to_wsgi_app(
235 235 self, baseapp, request_stub):
236 236 """
237 237 Check that a pull action to a shadow repo is propagated to the
238 238 underlying wsgi app.
239 239 """
240 240 controller = StubVCSController(
241 241 baseapp.config.get_settings(), request_stub.registry)
242 controller._check_ssl = mock.Mock()
243 242 controller.is_shadow_repo = True
244 243 controller._action = 'pull'
245 244 controller._is_shadow_repo_dir = True
246 245 controller.stub_response_body = (b'dummy body value',)
247 246 controller._get_default_cache_ttl = mock.Mock(
248 247 return_value=(False, 0))
249 248
250 249 environ_stub = {
251 250 'HTTP_HOST': 'test.example.com',
252 251 'HTTP_ACCEPT': 'application/mercurial',
253 252 'REQUEST_METHOD': 'GET',
254 253 'wsgi.url_scheme': 'http',
255 254 }
256 255
257 256 response = controller(environ_stub, mock.Mock())
258 257 response_body = b''.join(response)
259 258
260 259 # Assert that we got the response from the wsgi app.
261 260 assert response_body == b''.join(controller.stub_response_body)
262 261
263 262 def test_pull_on_shadow_repo_that_is_missing(self, baseapp, request_stub):
264 263 """
265 264 Check that a pull action to a shadow repo is propagated to the
266 265 underlying wsgi app.
267 266 """
268 267 controller = StubVCSController(
269 268 baseapp.config.get_settings(), request_stub.registry)
270 controller._check_ssl = mock.Mock()
271 269 controller.is_shadow_repo = True
272 270 controller._action = 'pull'
273 271 controller._is_shadow_repo_dir = False
274 272 controller.stub_response_body = (b'dummy body value',)
275 273 environ_stub = {
276 274 'HTTP_HOST': 'test.example.com',
277 275 'HTTP_ACCEPT': 'application/mercurial',
278 276 'REQUEST_METHOD': 'GET',
279 277 'wsgi.url_scheme': 'http',
280 278 }
281 279
282 280 response = controller(environ_stub, mock.Mock())
283 281 response_body = b''.join(response)
284 282
285 283 # Assert that we got the response from the wsgi app.
286 284 assert b'404 Not Found' in response_body
287 285
288 286 def test_push_on_shadow_repo_raises(self, baseapp, request_stub):
289 287 """
290 288 Check that a push action to a shadow repo is aborted.
291 289 """
292 290 controller = StubVCSController(
293 291 baseapp.config.get_settings(), request_stub.registry)
294 controller._check_ssl = mock.Mock()
295 292 controller.is_shadow_repo = True
296 293 controller._action = 'push'
297 294 controller.stub_response_body = (b'dummy body value',)
298 295 environ_stub = {
299 296 'HTTP_HOST': 'test.example.com',
300 297 'HTTP_ACCEPT': 'application/mercurial',
301 298 'REQUEST_METHOD': 'GET',
302 299 'wsgi.url_scheme': 'http',
303 300 }
304 301
305 302 response = controller(environ_stub, mock.Mock())
306 303 response_body = b''.join(response)
307 304
308 305 assert response_body != controller.stub_response_body
309 306 # Assert that a 406 error is returned.
310 307 assert b'406 Not Acceptable' in response_body
311 308
312 309 def test_set_repo_names_no_shadow(self, baseapp, request_stub):
313 310 """
314 311 Check that the set_repo_names method sets all names to the one returned
315 312 by the _get_repository_name method on a request to a non shadow repo.
316 313 """
317 314 environ_stub = {}
318 315 controller = StubVCSController(
319 316 baseapp.config.get_settings(), request_stub.registry)
320 317 controller._name = 'RepoGroup/MyRepo'
321 318 controller.set_repo_names(environ_stub)
322 319 assert not controller.is_shadow_repo
323 320 assert (controller.url_repo_name ==
324 321 controller.acl_repo_name ==
325 322 controller.vcs_repo_name ==
326 323 controller._get_repository_name(environ_stub))
327 324
328 325 def test_set_repo_names_with_shadow(
329 326 self, baseapp, pr_util, config_stub, request_stub):
330 327 """
331 328 Check that the set_repo_names method sets correct names on a request
332 329 to a shadow repo.
333 330 """
334 331 from rhodecode.model.pull_request import PullRequestModel
335 332
336 333 pull_request = pr_util.create_pull_request()
337 334 shadow_url = '{target}/{pr_segment}/{pr_id}/{shadow_segment}'.format(
338 335 target=pull_request.target_repo.repo_name,
339 336 pr_id=pull_request.pull_request_id,
340 337 pr_segment=TestShadowRepoRegularExpression.pr_segment,
341 338 shadow_segment=TestShadowRepoRegularExpression.shadow_segment)
342 339 controller = StubVCSController(
343 340 baseapp.config.get_settings(), request_stub.registry)
344 341 controller._name = shadow_url
345 342 controller.set_repo_names({})
346 343
347 344 # Get file system path to shadow repo for assertions.
348 345 workspace_id = PullRequestModel()._workspace_id(pull_request)
349 346 vcs_repo_name = pull_request.target_repo.get_shadow_repository_path(workspace_id)
350 347
351 348 assert controller.vcs_repo_name == vcs_repo_name
352 349 assert controller.url_repo_name == shadow_url
353 350 assert controller.acl_repo_name == pull_request.target_repo.repo_name
354 351 assert controller.is_shadow_repo
355 352
356 353 def test_set_repo_names_with_shadow_but_missing_pr(
357 354 self, baseapp, pr_util, config_stub, request_stub):
358 355 """
359 356 Checks that the set_repo_names method enforces matching target repos
360 357 and pull request IDs.
361 358 """
362 359 pull_request = pr_util.create_pull_request()
363 360 shadow_url = '{target}/{pr_segment}/{pr_id}/{shadow_segment}'.format(
364 361 target=pull_request.target_repo.repo_name,
365 362 pr_id=999999999,
366 363 pr_segment=TestShadowRepoRegularExpression.pr_segment,
367 364 shadow_segment=TestShadowRepoRegularExpression.shadow_segment)
368 365 controller = StubVCSController(
369 366 baseapp.config.get_settings(), request_stub.registry)
370 367 controller._name = shadow_url
371 368 controller.set_repo_names({})
372 369
373 370 assert not controller.is_shadow_repo
374 371 assert (controller.url_repo_name ==
375 372 controller.acl_repo_name ==
376 373 controller.vcs_repo_name)
377 374
378 375
379 376 @pytest.mark.usefixtures('baseapp')
380 377 class TestGenerateVcsResponse(object):
381 378
382 379 def test_ensures_that_start_response_is_called_early_enough(self):
383 380 self.call_controller_with_response_body(iter(['a', 'b']))
384 381 assert self.start_response.called
385 382
386 383 def test_invalidates_cache_after_body_is_consumed(self):
387 384 result = self.call_controller_with_response_body(iter(['a', 'b']))
388 385 assert not self.was_cache_invalidated()
389 386 # Consume the result
390 387 list(result)
391 388 assert self.was_cache_invalidated()
392 389
393 390 def test_raises_unknown_exceptions(self):
394 391 result = self.call_controller_with_response_body(
395 392 self.raise_result_iter(vcs_kind='unknown'))
396 393 with pytest.raises(Exception):
397 394 list(result)
398 395
399 396 def call_controller_with_response_body(self, response_body):
400 397 settings = {
401 398 'base_path': 'fake_base_path',
402 'vcs.hooks.protocol': 'http',
399 'vcs.hooks.protocol.v2': 'celery',
403 400 'vcs.hooks.direct_calls': False,
404 401 }
405 402 registry = AttributeDict()
406 403 controller = StubVCSController(settings, registry)
407 404 controller._invalidate_cache = mock.Mock()
408 405 controller.stub_response_body = response_body
409 406 self.start_response = mock.Mock()
410 407 result = controller._generate_vcs_response(
411 408 environ={}, start_response=self.start_response,
412 409 repo_path='fake_repo_path',
413 410 extras={}, action='push')
414 411 self.controller = controller
415 412 return result
416 413
417 414 def raise_result_iter(self, vcs_kind='repo_locked'):
418 415 """
419 416 Simulates an exception due to a vcs raised exception if kind vcs_kind
420 417 """
421 418 raise self.vcs_exception(vcs_kind=vcs_kind)
422 419 yield "never_reached"
423 420
424 421 def vcs_exception(self, vcs_kind='repo_locked'):
425 422 locked_exception = Exception('TEST_MESSAGE')
426 423 locked_exception._vcs_kind = vcs_kind
427 424 return locked_exception
428 425
429 426 def was_cache_invalidated(self):
430 427 return self.controller._invalidate_cache.called
431 428
432 429
433 430 class TestInitializeGenerator(object):
434 431
435 432 def test_drains_first_element(self):
436 433 gen = self.factory(['__init__', 1, 2])
437 434 result = list(gen)
438 435 assert result == [1, 2]
439 436
440 437 @pytest.mark.parametrize('values', [
441 438 [],
442 439 [1, 2],
443 440 ])
444 441 def test_raises_value_error(self, values):
445 442 with pytest.raises(ValueError):
446 443 self.factory(values)
447 444
448 445 @simplevcs.initialize_generator
449 446 def factory(self, iterable):
450 447 for elem in iterable:
451 448 yield elem
@@ -1,489 +1,490 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import multiprocessing
21 21 import os
22 22
23 23 import mock
24 24 import py
25 25 import pytest
26 26
27 27 from rhodecode.lib import caching_query
28 28 from rhodecode.lib import utils
29 29 from rhodecode.lib.str_utils import safe_bytes
30 30 from rhodecode.model import settings
31 31 from rhodecode.model import db
32 32 from rhodecode.model import meta
33 33 from rhodecode.model.repo import RepoModel
34 34 from rhodecode.model.repo_group import RepoGroupModel
35 35 from rhodecode.model.settings import UiSetting, SettingsModel
36 36 from rhodecode.tests.fixture import Fixture
37 37 from rhodecode_tools.lib.hash_utils import md5_safe
38 38 from rhodecode.lib.ext_json import json
39 39
40 40 fixture = Fixture()
41 41
42 42
43 43 def extract_hooks(config):
44 44 """Return a dictionary with the hook entries of the given config."""
45 45 hooks = {}
46 46 config_items = config.serialize()
47 47 for section, name, value in config_items:
48 48 if section != 'hooks':
49 49 continue
50 50 hooks[name] = value
51 51
52 52 return hooks
53 53
54 54
55 55 def disable_hooks(request, hooks):
56 56 """Disables the given hooks from the UI settings."""
57 57 session = meta.Session()
58 58
59 59 model = SettingsModel()
60 60 for hook_key in hooks:
61 61 sett = model.get_ui_by_key(hook_key)
62 62 sett.ui_active = False
63 63 session.add(sett)
64 64
65 65 # Invalidate cache
66 66 ui_settings = session.query(db.RhodeCodeUi).options(
67 67 caching_query.FromCache('sql_cache_short', 'get_hg_ui_settings'))
68 68
69 69 meta.cache.invalidate(
70 70 ui_settings, {},
71 71 caching_query.FromCache('sql_cache_short', 'get_hg_ui_settings'))
72 72
73 73 ui_settings = session.query(db.RhodeCodeUi).options(
74 74 caching_query.FromCache('sql_cache_short', 'get_hook_settings'))
75 75
76 76 meta.cache.invalidate(
77 77 ui_settings, {},
78 78 caching_query.FromCache('sql_cache_short', 'get_hook_settings'))
79 79
80 80 @request.addfinalizer
81 81 def rollback():
82 82 session.rollback()
83 83
84 84
85 85 HOOK_PRE_PUSH = db.RhodeCodeUi.HOOK_PRE_PUSH
86 86 HOOK_PRETX_PUSH = db.RhodeCodeUi.HOOK_PRETX_PUSH
87 87 HOOK_PUSH = db.RhodeCodeUi.HOOK_PUSH
88 88 HOOK_PRE_PULL = db.RhodeCodeUi.HOOK_PRE_PULL
89 89 HOOK_PULL = db.RhodeCodeUi.HOOK_PULL
90 90 HOOK_REPO_SIZE = db.RhodeCodeUi.HOOK_REPO_SIZE
91 91 HOOK_PUSH_KEY = db.RhodeCodeUi.HOOK_PUSH_KEY
92 92
93 93 HG_HOOKS = frozenset(
94 94 (HOOK_PRE_PULL, HOOK_PULL, HOOK_PRE_PUSH, HOOK_PRETX_PUSH, HOOK_PUSH,
95 95 HOOK_REPO_SIZE, HOOK_PUSH_KEY))
96 96
97 97
98 98 @pytest.mark.parametrize('disabled_hooks,expected_hooks', [
99 99 ([], HG_HOOKS),
100 100 (HG_HOOKS, []),
101 101
102 102 ([HOOK_PRE_PUSH, HOOK_PRETX_PUSH, HOOK_REPO_SIZE, HOOK_PUSH_KEY], [HOOK_PRE_PULL, HOOK_PULL, HOOK_PUSH]),
103 103
104 104 # When a pull/push hook is disabled, its pre-pull/push counterpart should
105 105 # be disabled too.
106 106 ([HOOK_PUSH], [HOOK_PRE_PULL, HOOK_PULL, HOOK_REPO_SIZE]),
107 107 ([HOOK_PULL], [HOOK_PRE_PUSH, HOOK_PRETX_PUSH, HOOK_PUSH, HOOK_REPO_SIZE,
108 108 HOOK_PUSH_KEY]),
109 109 ])
110 110 def test_make_db_config_hg_hooks(baseapp, request, disabled_hooks,
111 111 expected_hooks):
112 112 disable_hooks(request, disabled_hooks)
113 113
114 114 config = utils.make_db_config()
115 115 hooks = extract_hooks(config)
116 116
117 117 assert set(hooks.keys()).intersection(HG_HOOKS) == set(expected_hooks)
118 118
119 119
120 120 @pytest.mark.parametrize('disabled_hooks,expected_hooks', [
121 121 ([], ['pull', 'push']),
122 122 ([HOOK_PUSH], ['pull']),
123 123 ([HOOK_PULL], ['push']),
124 124 ([HOOK_PULL, HOOK_PUSH], []),
125 125 ])
126 126 def test_get_enabled_hook_classes(disabled_hooks, expected_hooks):
127 127 hook_keys = (HOOK_PUSH, HOOK_PULL)
128 128 ui_settings = [
129 129 ('hooks', key, 'some value', key not in disabled_hooks)
130 130 for key in hook_keys]
131 131
132 132 result = utils.get_enabled_hook_classes(ui_settings)
133 133 assert sorted(result) == expected_hooks
134 134
135 135
136 136 def test_get_filesystem_repos_finds_repos(tmpdir, baseapp):
137 137 _stub_git_repo(tmpdir.ensure('repo', dir=True))
138 138 repos = list(utils.get_filesystem_repos(str(tmpdir)))
139 139 assert repos == [('repo', ('git', tmpdir.join('repo')))]
140 140
141 141
142 142 def test_get_filesystem_repos_skips_directories(tmpdir, baseapp):
143 143 tmpdir.ensure('not-a-repo', dir=True)
144 144 repos = list(utils.get_filesystem_repos(str(tmpdir)))
145 145 assert repos == []
146 146
147 147
148 148 def test_get_filesystem_repos_skips_directories_with_repos(tmpdir, baseapp):
149 149 _stub_git_repo(tmpdir.ensure('subdir/repo', dir=True))
150 150 repos = list(utils.get_filesystem_repos(str(tmpdir)))
151 151 assert repos == []
152 152
153 153
154 154 def test_get_filesystem_repos_finds_repos_in_subdirectories(tmpdir, baseapp):
155 155 _stub_git_repo(tmpdir.ensure('subdir/repo', dir=True))
156 156 repos = list(utils.get_filesystem_repos(str(tmpdir), recursive=True))
157 157 assert repos == [('subdir/repo', ('git', tmpdir.join('subdir', 'repo')))]
158 158
159 159
160 160 def test_get_filesystem_repos_skips_names_starting_with_dot(tmpdir):
161 161 _stub_git_repo(tmpdir.ensure('.repo', dir=True))
162 162 repos = list(utils.get_filesystem_repos(str(tmpdir)))
163 163 assert repos == []
164 164
165 165
166 166 def test_get_filesystem_repos_skips_files(tmpdir):
167 167 tmpdir.ensure('test-file')
168 168 repos = list(utils.get_filesystem_repos(str(tmpdir)))
169 169 assert repos == []
170 170
171 171
172 172 def test_get_filesystem_repos_skips_removed_repositories(tmpdir):
173 173 removed_repo_name = 'rm__00000000_000000_000000__.stub'
174 174 assert utils.REMOVED_REPO_PAT.match(removed_repo_name)
175 175 _stub_git_repo(tmpdir.ensure(removed_repo_name, dir=True))
176 176 repos = list(utils.get_filesystem_repos(str(tmpdir)))
177 177 assert repos == []
178 178
179 179
180 180 def _stub_git_repo(repo_path):
181 181 """
182 182 Make `repo_path` look like a Git repository.
183 183 """
184 184 repo_path.ensure('.git', dir=True)
185 185
186 186
187 187 def test_get_dirpaths_returns_all_paths_on_str(tmpdir):
188 188 tmpdir.ensure('test-file')
189 189 tmpdir.ensure('test-file-1')
190 190 tmp_path = str(tmpdir)
191 191 dirpaths = utils.get_dirpaths(tmp_path)
192 192 assert list(sorted(dirpaths)) == ['test-file', 'test-file-1']
193 193
194 194
195 195 def test_get_dirpaths_returns_all_paths_on_bytes(tmpdir):
196 196 tmpdir.ensure('test-file-bytes')
197 197 tmp_path = str(tmpdir)
198 198 dirpaths = utils.get_dirpaths(safe_bytes(tmp_path))
199 199 assert list(sorted(dirpaths)) == [b'test-file-bytes']
200 200
201 201
202 202 def test_get_dirpaths_returns_all_paths_bytes(
203 203 tmpdir, platform_encodes_filenames):
204 204 if platform_encodes_filenames:
205 205 pytest.skip("This platform seems to encode filenames.")
206 206 tmpdir.ensure('repo-a-umlaut-\xe4')
207 207 dirpaths = utils.get_dirpaths(str(tmpdir))
208 208 assert dirpaths == ['repo-a-umlaut-\xe4']
209 209
210 210
211 211 def test_get_dirpaths_skips_paths_it_cannot_decode(
212 212 tmpdir, platform_encodes_filenames):
213 213 if platform_encodes_filenames:
214 214 pytest.skip("This platform seems to encode filenames.")
215 215 path_with_latin1 = 'repo-a-umlaut-\xe4'
216 216 tmp_path = str(tmpdir.ensure(path_with_latin1))
217 217 dirpaths = utils.get_dirpaths(tmp_path)
218 218 assert dirpaths == []
219 219
220 220
221 221 @pytest.fixture(scope='session')
222 222 def platform_encodes_filenames():
223 223 """
224 224 Boolean indicator if the current platform changes filename encodings.
225 225 """
226 226 path_with_latin1 = 'repo-a-umlaut-\xe4'
227 227 tmpdir = py.path.local.mkdtemp()
228 228 tmpdir.ensure(path_with_latin1)
229 229 read_path = tmpdir.listdir()[0].basename
230 230 tmpdir.remove()
231 231 return path_with_latin1 != read_path
232 232
233 233
234 234 def test_repo2db_mapper_groups(repo_groups):
235 235 session = meta.Session()
236 236 zombie_group, parent_group, child_group = repo_groups
237 237 zombie_path = os.path.join(
238 238 RepoGroupModel().repos_path, zombie_group.full_path)
239 239 os.rmdir(zombie_path)
240 240
241 241 # Avoid removing test repos when calling repo2db_mapper
242 242 repo_list = {
243 243 repo.repo_name: 'test' for repo in session.query(db.Repository).all()
244 244 }
245 245 utils.repo2db_mapper(repo_list, remove_obsolete=True)
246 246
247 247 groups_in_db = session.query(db.RepoGroup).all()
248 248 assert child_group in groups_in_db
249 249 assert parent_group in groups_in_db
250 250 assert zombie_path not in groups_in_db
251 251
252 252
253 253 def test_repo2db_mapper_enables_largefiles(backend):
254 254 repo = backend.create_repo()
255 255 repo_list = {repo.repo_name: 'test'}
256 256 with mock.patch('rhodecode.model.db.Repository.scm_instance') as scm_mock:
257 257 utils.repo2db_mapper(repo_list, remove_obsolete=False)
258 258 _, kwargs = scm_mock.call_args
259 259 assert kwargs['config'].get('extensions', 'largefiles') == ''
260 260
261 261
262 262 @pytest.mark.backends("git", "svn")
263 263 def test_repo2db_mapper_installs_hooks_for_repos_in_db(backend):
264 264 repo = backend.create_repo()
265 265 repo_list = {repo.repo_name: 'test'}
266 266 utils.repo2db_mapper(repo_list, remove_obsolete=False)
267 267
268 268
269 269 @pytest.mark.backends("git", "svn")
270 270 def test_repo2db_mapper_installs_hooks_for_newly_added_repos(backend):
271 271 repo = backend.create_repo()
272 272 RepoModel().delete(repo, fs_remove=False)
273 273 meta.Session().commit()
274 274 repo_list = {repo.repo_name: repo.scm_instance()}
275 275 utils.repo2db_mapper(repo_list, remove_obsolete=False)
276 276
277 277
278 278 class TestPasswordChanged(object):
279 279
280 280 def setup_method(self):
281 281 self.session = {
282 282 'rhodecode_user': {
283 283 'password': '0cc175b9c0f1b6a831c399e269772661'
284 284 }
285 285 }
286 286 self.auth_user = mock.Mock()
287 287 self.auth_user.userame = 'test'
288 288 self.auth_user.password = 'abc123'
289 289
290 290 def test_returns_false_for_default_user(self):
291 291 self.auth_user.username = db.User.DEFAULT_USER
292 292 result = utils.password_changed(self.auth_user, self.session)
293 293 assert result is False
294 294
295 295 def test_returns_false_if_password_was_not_changed(self):
296 296 self.session['rhodecode_user']['password'] = md5_safe(
297 297 self.auth_user.password)
298 298 result = utils.password_changed(self.auth_user, self.session)
299 299 assert result is False
300 300
301 301 def test_returns_true_if_password_was_changed(self):
302 302 result = utils.password_changed(self.auth_user, self.session)
303 303 assert result is True
304 304
305 305 def test_returns_true_if_auth_user_password_is_empty(self):
306 306 self.auth_user.password = None
307 307 result = utils.password_changed(self.auth_user, self.session)
308 308 assert result is True
309 309
310 310 def test_returns_true_if_session_password_is_empty(self):
311 311 self.session['rhodecode_user'].pop('password')
312 312 result = utils.password_changed(self.auth_user, self.session)
313 313 assert result is True
314 314
315 315
316 316 class TestReadOpenSourceLicenses(object):
317 317 def test_success(self):
318 318 utils._license_cache = None
319 319 json_data = '''
320 320 {
321 321 "python2.7-pytest-2.7.1": {"UNKNOWN": null},
322 322 "python2.7-Markdown-2.6.2": {
323 323 "BSD-3-Clause": "http://spdx.org/licenses/BSD-3-Clause"
324 324 }
325 325 }
326 326 '''
327 327 resource_string_patch = mock.patch.object(
328 328 utils.pkg_resources, 'resource_string', return_value=json_data)
329 329 with resource_string_patch:
330 330 result = utils.read_opensource_licenses()
331 331 assert result == json.loads(json_data)
332 332
333 333 def test_caching(self):
334 334 utils._license_cache = {
335 335 "python2.7-pytest-2.7.1": {
336 336 "UNKNOWN": None
337 337 },
338 338 "python2.7-Markdown-2.6.2": {
339 339 "BSD-3-Clause": "http://spdx.org/licenses/BSD-3-Clause"
340 340 }
341 341 }
342 342 resource_patch = mock.patch.object(
343 343 utils.pkg_resources, 'resource_string', side_effect=Exception)
344 344 json_patch = mock.patch.object(
345 345 utils.json, 'loads', side_effect=Exception)
346 346
347 347 with resource_patch as resource_mock, json_patch as json_mock:
348 348 result = utils.read_opensource_licenses()
349 349
350 350 assert resource_mock.call_count == 0
351 351 assert json_mock.call_count == 0
352 352 assert result == utils._license_cache
353 353
354 354 def test_licenses_file_contains_no_unknown_licenses(self):
355 355 utils._license_cache = None
356 356 result = utils.read_opensource_licenses()
357 357
358 358 for license_data in result:
359 359 if isinstance(license_data["license"], list):
360 360 for lic_data in license_data["license"]:
361 361 assert 'UNKNOWN' not in lic_data["fullName"]
362 362 else:
363 363 full_name = license_data.get("fullName") or license_data
364 364 assert 'UNKNOWN' not in full_name
365 365
366 366
367 367 class TestMakeDbConfig(object):
368 368 def test_data_from_config_data_from_db_returned(self):
369 369 test_data = [
370 370 ('section1', 'option1', 'value1'),
371 371 ('section2', 'option2', 'value2'),
372 372 ('section3', 'option3', 'value3'),
373 373 ]
374 with mock.patch.object(utils, 'config_data_from_db') as config_mock:
374 with mock.patch.object(utils, 'prepare_config_data') as config_mock:
375 375 config_mock.return_value = test_data
376 376 kwargs = {'clear_session': False, 'repo': 'test_repo'}
377 377 result = utils.make_db_config(**kwargs)
378 378 config_mock.assert_called_once_with(**kwargs)
379 379 for section, option, expected_value in test_data:
380 380 value = result.get(section, option)
381 381 assert value == expected_value
382 382
383 383
384 class TestConfigDataFromDb(object):
385 def test_config_data_from_db_returns_active_settings(self):
384 class TestPrepareConfigData(object):
385 def test_prepare_config_data_returns_active_settings(self):
386 386 test_data = [
387 387 UiSetting('section1', 'option1', 'value1', True),
388 388 UiSetting('section2', 'option2', 'value2', True),
389 389 UiSetting('section3', 'option3', 'value3', False),
390 390 ]
391 391 repo_name = 'test_repo'
392 392
393 393 model_patch = mock.patch.object(settings, 'VcsSettingsModel')
394 394 hooks_patch = mock.patch.object(
395 395 utils, 'get_enabled_hook_classes',
396 396 return_value=['pull', 'push', 'repo_size'])
397 397 with model_patch as model_mock, hooks_patch:
398 398 instance_mock = mock.Mock()
399 399 model_mock.return_value = instance_mock
400 400 instance_mock.get_ui_settings.return_value = test_data
401 result = utils.config_data_from_db(
401 result = utils.prepare_config_data(
402 402 clear_session=False, repo=repo_name)
403 403
404 404 self._assert_repo_name_passed(model_mock, repo_name)
405 405
406 406 expected_result = [
407 407 ('section1', 'option1', 'value1'),
408 408 ('section2', 'option2', 'value2'),
409 409 ]
410 assert result == expected_result
410 # We have extra config items returned, so we're ignoring two last items
411 assert result[:2] == expected_result
411 412
412 413 def _assert_repo_name_passed(self, model_mock, repo_name):
413 414 assert model_mock.call_count == 1
414 415 call_args, call_kwargs = model_mock.call_args
415 416 assert call_kwargs['repo'] == repo_name
416 417
417 418
418 419 class TestIsDirWritable(object):
419 420 def test_returns_false_when_not_writable(self):
420 421 with mock.patch('builtins.open', side_effect=OSError):
421 422 assert not utils._is_dir_writable('/stub-path')
422 423
423 424 def test_returns_true_when_writable(self, tmpdir):
424 425 assert utils._is_dir_writable(str(tmpdir))
425 426
426 427 def test_is_safe_against_race_conditions(self, tmpdir):
427 428 workers = multiprocessing.Pool()
428 429 directories = [str(tmpdir)] * 10
429 430 workers.map(utils._is_dir_writable, directories)
430 431
431 432
432 433 class TestGetEnabledHooks(object):
433 434 def test_only_active_hooks_are_enabled(self):
434 435 ui_settings = [
435 436 UiSetting('hooks', db.RhodeCodeUi.HOOK_PUSH, 'value', True),
436 437 UiSetting('hooks', db.RhodeCodeUi.HOOK_REPO_SIZE, 'value', True),
437 438 UiSetting('hooks', db.RhodeCodeUi.HOOK_PULL, 'value', False)
438 439 ]
439 440 result = utils.get_enabled_hook_classes(ui_settings)
440 441 assert result == ['push', 'repo_size']
441 442
442 443 def test_all_hooks_are_enabled(self):
443 444 ui_settings = [
444 445 UiSetting('hooks', db.RhodeCodeUi.HOOK_PUSH, 'value', True),
445 446 UiSetting('hooks', db.RhodeCodeUi.HOOK_REPO_SIZE, 'value', True),
446 447 UiSetting('hooks', db.RhodeCodeUi.HOOK_PULL, 'value', True)
447 448 ]
448 449 result = utils.get_enabled_hook_classes(ui_settings)
449 450 assert result == ['push', 'repo_size', 'pull']
450 451
451 452 def test_no_enabled_hooks_when_no_hook_settings_are_found(self):
452 453 ui_settings = []
453 454 result = utils.get_enabled_hook_classes(ui_settings)
454 455 assert result == []
455 456
456 457
457 458 def test_obfuscate_url_pw():
458 459 from rhodecode.lib.utils2 import obfuscate_url_pw
459 460 engine = u'/home/repos/malmö'
460 461 assert obfuscate_url_pw(engine)
461 462
462 463
463 464 @pytest.mark.parametrize("test_ua, expected", [
464 465 ("", ""),
465 466 ('"quoted"', 'quoted'),
466 467 ('internal-merge', 'internal-merge'),
467 468 ('hg/internal-merge', 'hg/internal-merge'),
468 469 ('git/internal-merge', 'git/internal-merge'),
469 470
470 471 # git
471 472 ('git/2.10.1 (Apple Git-78)', 'git/2.10.1'),
472 473 ('GiT/2.37.2.windows.2', 'git/2.37.2'),
473 474 ('git/2.35.1 (Microsoft Windows NT 10.0.19044.0; Win32NT x64) CLR/4.0.30319 VS16/16.0.0', 'git/2.35.1'),
474 475 ('ssh-user-agent', 'ssh-user-agent'),
475 476 ('git/ssh-user-agent', 'git/ssh-user-agent'),
476 477
477 478
478 479 # hg
479 480 ('mercurial/proto-1.0 (Mercurial 4.2)', 'mercurial/4.2'),
480 481 ('mercurial/proto-1.0', ''),
481 482 ('mercurial/proto-1.0 (Mercurial 3.9.2)', 'mercurial/3.9.2'),
482 483 ('mercurial/ssh-user-agent', 'mercurial/ssh-user-agent'),
483 484 ('mercurial/proto-1.0 (Mercurial 5.8rc0)', 'mercurial/5.8rc0'),
484 485
485 486
486 487 ])
487 488 def test_user_agent_normalizer(test_ua, expected):
488 489 from rhodecode.lib.utils2 import user_agent_normalizer
489 490 assert user_agent_normalizer(test_ua, safe=False) == expected
@@ -1,1114 +1,1097 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import mock
21 21 import pytest
22 22
23 23 from rhodecode.lib.utils2 import str2bool
24 24 from rhodecode.model.meta import Session
25 25 from rhodecode.model.settings import VcsSettingsModel, UiSetting
26 26
27 27
28 28 HOOKS_FORM_DATA = {
29 29 'hooks_changegroup_repo_size': True,
30 30 'hooks_changegroup_push_logger': True,
31 31 'hooks_outgoing_pull_logger': True
32 32 }
33 33
34 34 SVN_FORM_DATA = {
35 35 'new_svn_branch': 'test-branch',
36 36 'new_svn_tag': 'test-tag'
37 37 }
38 38
39 39 GENERAL_FORM_DATA = {
40 40 'rhodecode_pr_merge_enabled': True,
41 41 'rhodecode_use_outdated_comments': True,
42 42 'rhodecode_hg_use_rebase_for_merging': True,
43 43 'rhodecode_hg_close_branch_before_merging': True,
44 44 'rhodecode_git_use_rebase_for_merging': True,
45 45 'rhodecode_git_close_branch_before_merging': True,
46 46 'rhodecode_diff_cache': True,
47 47 }
48 48
49 49
50 50 class TestInheritGlobalSettingsProperty(object):
51 51 def test_get_raises_exception_when_repository_not_specified(self):
52 52 model = VcsSettingsModel()
53 53 with pytest.raises(Exception) as exc_info:
54 54 model.inherit_global_settings
55 55 assert str(exc_info.value) == 'Repository is not specified'
56 56
57 57 def test_true_is_returned_when_value_is_not_found(self, repo_stub):
58 58 model = VcsSettingsModel(repo=repo_stub.repo_name)
59 59 assert model.inherit_global_settings is True
60 60
61 61 def test_value_is_returned(self, repo_stub, settings_util):
62 62 model = VcsSettingsModel(repo=repo_stub.repo_name)
63 63 settings_util.create_repo_rhodecode_setting(
64 64 repo_stub, VcsSettingsModel.INHERIT_SETTINGS, False, 'bool')
65 65 assert model.inherit_global_settings is False
66 66
67 67 def test_value_is_set(self, repo_stub):
68 68 model = VcsSettingsModel(repo=repo_stub.repo_name)
69 69 model.inherit_global_settings = False
70 70 setting = model.repo_settings.get_setting_by_name(
71 71 VcsSettingsModel.INHERIT_SETTINGS)
72 72 try:
73 73 assert setting.app_settings_type == 'bool'
74 74 assert setting.app_settings_value is False
75 75 finally:
76 76 Session().delete(setting)
77 77 Session().commit()
78 78
79 79 def test_set_raises_exception_when_repository_not_specified(self):
80 80 model = VcsSettingsModel()
81 81 with pytest.raises(Exception) as exc_info:
82 82 model.inherit_global_settings = False
83 83 assert str(exc_info.value) == 'Repository is not specified'
84 84
85 85
86 86 class TestVcsSettingsModel(object):
87 87 def test_global_svn_branch_patterns(self):
88 88 model = VcsSettingsModel()
89 89 expected_result = {'test': 'test'}
90 90 with mock.patch.object(model, 'global_settings') as settings_mock:
91 91 get_settings = settings_mock.get_ui_by_section
92 92 get_settings.return_value = expected_result
93 93 settings_mock.return_value = expected_result
94 94 result = model.get_global_svn_branch_patterns()
95 95
96 96 get_settings.assert_called_once_with(model.SVN_BRANCH_SECTION)
97 97 assert expected_result == result
98 98
99 99 def test_repo_svn_branch_patterns(self):
100 100 model = VcsSettingsModel()
101 101 expected_result = {'test': 'test'}
102 102 with mock.patch.object(model, 'repo_settings') as settings_mock:
103 103 get_settings = settings_mock.get_ui_by_section
104 104 get_settings.return_value = expected_result
105 105 settings_mock.return_value = expected_result
106 106 result = model.get_repo_svn_branch_patterns()
107 107
108 108 get_settings.assert_called_once_with(model.SVN_BRANCH_SECTION)
109 109 assert expected_result == result
110 110
111 111 def test_repo_svn_branch_patterns_raises_exception_when_repo_is_not_set(
112 112 self):
113 113 model = VcsSettingsModel()
114 114 with pytest.raises(Exception) as exc_info:
115 115 model.get_repo_svn_branch_patterns()
116 116 assert str(exc_info.value) == 'Repository is not specified'
117 117
118 118 def test_global_svn_tag_patterns(self):
119 119 model = VcsSettingsModel()
120 120 expected_result = {'test': 'test'}
121 121 with mock.patch.object(model, 'global_settings') as settings_mock:
122 122 get_settings = settings_mock.get_ui_by_section
123 123 get_settings.return_value = expected_result
124 124 settings_mock.return_value = expected_result
125 125 result = model.get_global_svn_tag_patterns()
126 126
127 127 get_settings.assert_called_once_with(model.SVN_TAG_SECTION)
128 128 assert expected_result == result
129 129
130 130 def test_repo_svn_tag_patterns(self):
131 131 model = VcsSettingsModel()
132 132 expected_result = {'test': 'test'}
133 133 with mock.patch.object(model, 'repo_settings') as settings_mock:
134 134 get_settings = settings_mock.get_ui_by_section
135 135 get_settings.return_value = expected_result
136 136 settings_mock.return_value = expected_result
137 137 result = model.get_repo_svn_tag_patterns()
138 138
139 139 get_settings.assert_called_once_with(model.SVN_TAG_SECTION)
140 140 assert expected_result == result
141 141
142 142 def test_repo_svn_tag_patterns_raises_exception_when_repo_is_not_set(self):
143 143 model = VcsSettingsModel()
144 144 with pytest.raises(Exception) as exc_info:
145 145 model.get_repo_svn_tag_patterns()
146 146 assert str(exc_info.value) == 'Repository is not specified'
147 147
148 148 def test_get_global_settings(self):
149 149 expected_result = {'test': 'test'}
150 150 model = VcsSettingsModel()
151 151 with mock.patch.object(model, '_collect_all_settings') as collect_mock:
152 152 collect_mock.return_value = expected_result
153 153 result = model.get_global_settings()
154 154
155 155 collect_mock.assert_called_once_with(global_=True)
156 156 assert result == expected_result
157 157
158 158 def test_get_repo_settings(self, repo_stub):
159 159 model = VcsSettingsModel(repo=repo_stub.repo_name)
160 160 expected_result = {'test': 'test'}
161 161 with mock.patch.object(model, '_collect_all_settings') as collect_mock:
162 162 collect_mock.return_value = expected_result
163 163 result = model.get_repo_settings()
164 164
165 165 collect_mock.assert_called_once_with(global_=False)
166 166 assert result == expected_result
167 167
168 168 @pytest.mark.parametrize('settings, global_', [
169 169 ('global_settings', True),
170 170 ('repo_settings', False)
171 171 ])
172 172 def test_collect_all_settings(self, settings, global_):
173 173 model = VcsSettingsModel()
174 174 result_mock = self._mock_result()
175 175
176 176 settings_patch = mock.patch.object(model, settings)
177 177 with settings_patch as settings_mock:
178 178 settings_mock.get_ui_by_section_and_key.return_value = result_mock
179 179 settings_mock.get_setting_by_name.return_value = result_mock
180 180 result = model._collect_all_settings(global_=global_)
181 181
182 182 ui_settings = model.HG_SETTINGS + model.GIT_SETTINGS + model.HOOKS_SETTINGS
183 183 self._assert_get_settings_calls(
184 184 settings_mock, ui_settings, model.GENERAL_SETTINGS)
185 185 self._assert_collect_all_settings_result(
186 186 ui_settings, model.GENERAL_SETTINGS, result)
187 187
188 188 @pytest.mark.parametrize('settings, global_', [
189 189 ('global_settings', True),
190 190 ('repo_settings', False)
191 191 ])
192 192 def test_collect_all_settings_without_empty_value(self, settings, global_):
193 193 model = VcsSettingsModel()
194 194
195 195 settings_patch = mock.patch.object(model, settings)
196 196 with settings_patch as settings_mock:
197 197 settings_mock.get_ui_by_section_and_key.return_value = None
198 198 settings_mock.get_setting_by_name.return_value = None
199 199 result = model._collect_all_settings(global_=global_)
200 200
201 201 assert result == {}
202 202
203 203 def _mock_result(self):
204 204 result_mock = mock.Mock()
205 205 result_mock.ui_value = 'ui_value'
206 206 result_mock.ui_active = True
207 207 result_mock.app_settings_value = 'setting_value'
208 208 return result_mock
209 209
210 210 def _assert_get_settings_calls(
211 211 self, settings_mock, ui_settings, general_settings):
212 212 assert (
213 213 settings_mock.get_ui_by_section_and_key.call_count ==
214 214 len(ui_settings))
215 215 assert (
216 216 settings_mock.get_setting_by_name.call_count ==
217 217 len(general_settings))
218 218
219 219 for section, key in ui_settings:
220 220 expected_call = mock.call(section, key)
221 221 assert (
222 222 expected_call in
223 223 settings_mock.get_ui_by_section_and_key.call_args_list)
224 224
225 225 for name in general_settings:
226 226 expected_call = mock.call(name)
227 227 assert (
228 228 expected_call in
229 229 settings_mock.get_setting_by_name.call_args_list)
230 230
231 231 def _assert_collect_all_settings_result(
232 232 self, ui_settings, general_settings, result):
233 233 expected_result = {}
234 234 for section, key in ui_settings:
235 235 key = '{}_{}'.format(section, key.replace('.', '_'))
236 236
237 237 if section in ('extensions', 'hooks'):
238 238 value = True
239 239 elif key in ['vcs_git_lfs_enabled']:
240 240 value = True
241 241 else:
242 242 value = 'ui_value'
243 243 expected_result[key] = value
244 244
245 245 for name in general_settings:
246 246 key = 'rhodecode_' + name
247 247 expected_result[key] = 'setting_value'
248 248
249 249 assert expected_result == result
250 250
251 251
252 252 class TestCreateOrUpdateRepoHookSettings(object):
253 253 def test_create_when_no_repo_object_found(self, repo_stub):
254 254 model = VcsSettingsModel(repo=repo_stub.repo_name)
255 255
256 256 self._create_settings(model, HOOKS_FORM_DATA)
257 257
258 258 cleanup = []
259 259 try:
260 260 for section, key in model.HOOKS_SETTINGS:
261 261 ui = model.repo_settings.get_ui_by_section_and_key(
262 262 section, key)
263 263 assert ui.ui_active is True
264 264 cleanup.append(ui)
265 265 finally:
266 266 for ui in cleanup:
267 267 Session().delete(ui)
268 268 Session().commit()
269 269
270 270 def test_create_raises_exception_when_data_incomplete(self, repo_stub):
271 271 model = VcsSettingsModel(repo=repo_stub.repo_name)
272 272
273 273 deleted_key = 'hooks_changegroup_repo_size'
274 274 data = HOOKS_FORM_DATA.copy()
275 275 data.pop(deleted_key)
276 276
277 277 with pytest.raises(ValueError) as exc_info:
278 278 model.create_or_update_repo_hook_settings(data)
279 279 Session().commit()
280 280
281 281 msg = 'The given data does not contain {} key'.format(deleted_key)
282 282 assert str(exc_info.value) == msg
283 283
284 284 def test_update_when_repo_object_found(self, repo_stub, settings_util):
285 285 model = VcsSettingsModel(repo=repo_stub.repo_name)
286 286 for section, key in model.HOOKS_SETTINGS:
287 287 settings_util.create_repo_rhodecode_ui(
288 288 repo_stub, section, None, key=key, active=False)
289 289 model.create_or_update_repo_hook_settings(HOOKS_FORM_DATA)
290 290 Session().commit()
291 291
292 292 for section, key in model.HOOKS_SETTINGS:
293 293 ui = model.repo_settings.get_ui_by_section_and_key(section, key)
294 294 assert ui.ui_active is True
295 295
296 296 def _create_settings(self, model, data):
297 297 global_patch = mock.patch.object(model, 'global_settings')
298 298 global_setting = mock.Mock()
299 299 global_setting.ui_value = 'Test value'
300 300 with global_patch as global_mock:
301 301 global_mock.get_ui_by_section_and_key.return_value = global_setting
302 302 model.create_or_update_repo_hook_settings(HOOKS_FORM_DATA)
303 303 Session().commit()
304 304
305 305
306 306 class TestUpdateGlobalHookSettings(object):
307 307 def test_update_raises_exception_when_data_incomplete(self):
308 308 model = VcsSettingsModel()
309 309
310 310 deleted_key = 'hooks_changegroup_repo_size'
311 311 data = HOOKS_FORM_DATA.copy()
312 312 data.pop(deleted_key)
313 313
314 314 with pytest.raises(ValueError) as exc_info:
315 315 model.update_global_hook_settings(data)
316 316 Session().commit()
317 317
318 318 msg = 'The given data does not contain {} key'.format(deleted_key)
319 319 assert str(exc_info.value) == msg
320 320
321 321 def test_update_global_hook_settings(self, settings_util):
322 322 model = VcsSettingsModel()
323 323 setting_mock = mock.MagicMock()
324 324 setting_mock.ui_active = False
325 325 get_settings_patcher = mock.patch.object(
326 326 model.global_settings, 'get_ui_by_section_and_key',
327 327 return_value=setting_mock)
328 328 session_patcher = mock.patch('rhodecode.model.settings.Session')
329 329 with get_settings_patcher as get_settings_mock, session_patcher:
330 330 model.update_global_hook_settings(HOOKS_FORM_DATA)
331 331 Session().commit()
332 332
333 333 assert setting_mock.ui_active is True
334 334 assert get_settings_mock.call_count == 3
335 335
336 336
337 337 class TestCreateOrUpdateRepoGeneralSettings(object):
338 338 def test_calls_create_or_update_general_settings(self, repo_stub):
339 339 model = VcsSettingsModel(repo=repo_stub.repo_name)
340 340 create_patch = mock.patch.object(
341 341 model, '_create_or_update_general_settings')
342 342 with create_patch as create_mock:
343 343 model.create_or_update_repo_pr_settings(GENERAL_FORM_DATA)
344 344 Session().commit()
345 345
346 346 create_mock.assert_called_once_with(
347 347 model.repo_settings, GENERAL_FORM_DATA)
348 348
349 349 def test_raises_exception_when_repository_is_not_specified(self):
350 350 model = VcsSettingsModel()
351 351 with pytest.raises(Exception) as exc_info:
352 352 model.create_or_update_repo_pr_settings(GENERAL_FORM_DATA)
353 353 assert str(exc_info.value) == 'Repository is not specified'
354 354
355 355
356 356 class TestCreateOrUpdatGlobalGeneralSettings(object):
357 357 def test_calls_create_or_update_general_settings(self):
358 358 model = VcsSettingsModel()
359 359 create_patch = mock.patch.object(
360 360 model, '_create_or_update_general_settings')
361 361 with create_patch as create_mock:
362 362 model.create_or_update_global_pr_settings(GENERAL_FORM_DATA)
363 363 create_mock.assert_called_once_with(
364 364 model.global_settings, GENERAL_FORM_DATA)
365 365
366 366
367 367 class TestCreateOrUpdateGeneralSettings(object):
368 368 def test_create_when_no_repo_settings_found(self, repo_stub):
369 369 model = VcsSettingsModel(repo=repo_stub.repo_name)
370 370 model._create_or_update_general_settings(
371 371 model.repo_settings, GENERAL_FORM_DATA)
372 372
373 373 cleanup = []
374 374 try:
375 375 for name in model.GENERAL_SETTINGS:
376 376 setting = model.repo_settings.get_setting_by_name(name)
377 377 assert setting.app_settings_value is True
378 378 cleanup.append(setting)
379 379 finally:
380 380 for setting in cleanup:
381 381 Session().delete(setting)
382 382 Session().commit()
383 383
384 384 def test_create_raises_exception_when_data_incomplete(self, repo_stub):
385 385 model = VcsSettingsModel(repo=repo_stub.repo_name)
386 386
387 387 deleted_key = 'rhodecode_pr_merge_enabled'
388 388 data = GENERAL_FORM_DATA.copy()
389 389 data.pop(deleted_key)
390 390
391 391 with pytest.raises(ValueError) as exc_info:
392 392 model._create_or_update_general_settings(model.repo_settings, data)
393 393 Session().commit()
394 394
395 395 msg = 'The given data does not contain {} key'.format(deleted_key)
396 396 assert str(exc_info.value) == msg
397 397
398 398 def test_update_when_repo_setting_found(self, repo_stub, settings_util):
399 399 model = VcsSettingsModel(repo=repo_stub.repo_name)
400 400 for name in model.GENERAL_SETTINGS:
401 401 settings_util.create_repo_rhodecode_setting(
402 402 repo_stub, name, False, 'bool')
403 403
404 404 model._create_or_update_general_settings(
405 405 model.repo_settings, GENERAL_FORM_DATA)
406 406 Session().commit()
407 407
408 408 for name in model.GENERAL_SETTINGS:
409 409 setting = model.repo_settings.get_setting_by_name(name)
410 410 assert setting.app_settings_value is True
411 411
412 412
413 413 class TestCreateRepoSvnSettings(object):
414 414 def test_calls_create_svn_settings(self, repo_stub):
415 415 model = VcsSettingsModel(repo=repo_stub.repo_name)
416 416 with mock.patch.object(model, '_create_svn_settings') as create_mock:
417 417 model.create_repo_svn_settings(SVN_FORM_DATA)
418 418 Session().commit()
419 419
420 420 create_mock.assert_called_once_with(model.repo_settings, SVN_FORM_DATA)
421 421
422 422 def test_raises_exception_when_repository_is_not_specified(self):
423 423 model = VcsSettingsModel()
424 424 with pytest.raises(Exception) as exc_info:
425 425 model.create_repo_svn_settings(SVN_FORM_DATA)
426 426 Session().commit()
427 427
428 428 assert str(exc_info.value) == 'Repository is not specified'
429 429
430 430
431 431 class TestCreateSvnSettings(object):
432 432 def test_create(self, repo_stub):
433 433 model = VcsSettingsModel(repo=repo_stub.repo_name)
434 434 model._create_svn_settings(model.repo_settings, SVN_FORM_DATA)
435 435 Session().commit()
436 436
437 437 branch_ui = model.repo_settings.get_ui_by_section(
438 438 model.SVN_BRANCH_SECTION)
439 439 tag_ui = model.repo_settings.get_ui_by_section(
440 440 model.SVN_TAG_SECTION)
441 441
442 442 try:
443 443 assert len(branch_ui) == 1
444 444 assert len(tag_ui) == 1
445 445 finally:
446 446 Session().delete(branch_ui[0])
447 447 Session().delete(tag_ui[0])
448 448 Session().commit()
449 449
450 450 def test_create_tag(self, repo_stub):
451 451 model = VcsSettingsModel(repo=repo_stub.repo_name)
452 452 data = SVN_FORM_DATA.copy()
453 453 data.pop('new_svn_branch')
454 454 model._create_svn_settings(model.repo_settings, data)
455 455 Session().commit()
456 456
457 457 branch_ui = model.repo_settings.get_ui_by_section(
458 458 model.SVN_BRANCH_SECTION)
459 459 tag_ui = model.repo_settings.get_ui_by_section(
460 460 model.SVN_TAG_SECTION)
461 461
462 462 try:
463 463 assert len(branch_ui) == 0
464 464 assert len(tag_ui) == 1
465 465 finally:
466 466 Session().delete(tag_ui[0])
467 467 Session().commit()
468 468
469 469 def test_create_nothing_when_no_svn_settings_specified(self, repo_stub):
470 470 model = VcsSettingsModel(repo=repo_stub.repo_name)
471 471 model._create_svn_settings(model.repo_settings, {})
472 472 Session().commit()
473 473
474 474 branch_ui = model.repo_settings.get_ui_by_section(
475 475 model.SVN_BRANCH_SECTION)
476 476 tag_ui = model.repo_settings.get_ui_by_section(
477 477 model.SVN_TAG_SECTION)
478 478
479 479 assert len(branch_ui) == 0
480 480 assert len(tag_ui) == 0
481 481
482 482 def test_create_nothing_when_empty_settings_specified(self, repo_stub):
483 483 model = VcsSettingsModel(repo=repo_stub.repo_name)
484 484 data = {
485 485 'new_svn_branch': '',
486 486 'new_svn_tag': ''
487 487 }
488 488 model._create_svn_settings(model.repo_settings, data)
489 489 Session().commit()
490 490
491 491 branch_ui = model.repo_settings.get_ui_by_section(
492 492 model.SVN_BRANCH_SECTION)
493 493 tag_ui = model.repo_settings.get_ui_by_section(
494 494 model.SVN_TAG_SECTION)
495 495
496 496 assert len(branch_ui) == 0
497 497 assert len(tag_ui) == 0
498 498
499 499
500 500 class TestCreateOrUpdateUi(object):
501 501 def test_create(self, repo_stub):
502 502 model = VcsSettingsModel(repo=repo_stub.repo_name)
503 503 model._create_or_update_ui(
504 504 model.repo_settings, 'test-section', 'test-key', active=False,
505 505 value='False')
506 506 Session().commit()
507 507
508 508 created_ui = model.repo_settings.get_ui_by_section_and_key(
509 509 'test-section', 'test-key')
510 510
511 511 try:
512 512 assert created_ui.ui_active is False
513 513 assert str2bool(created_ui.ui_value) is False
514 514 finally:
515 515 Session().delete(created_ui)
516 516 Session().commit()
517 517
518 518 def test_update(self, repo_stub, settings_util):
519 519 model = VcsSettingsModel(repo=repo_stub.repo_name)
520 520 # care about only 3 first settings
521 521 largefiles, phases, evolve = model.HG_SETTINGS[:3]
522 522
523 523 section = 'test-section'
524 524 key = 'test-key'
525 525 settings_util.create_repo_rhodecode_ui(
526 526 repo_stub, section, 'True', key=key, active=True)
527 527
528 528 model._create_or_update_ui(
529 529 model.repo_settings, section, key, active=False, value='False')
530 530 Session().commit()
531 531
532 532 created_ui = model.repo_settings.get_ui_by_section_and_key(
533 533 section, key)
534 534 assert created_ui.ui_active is False
535 535 assert str2bool(created_ui.ui_value) is False
536 536
537 537
538 538 class TestCreateOrUpdateRepoHgSettings(object):
539 539 FORM_DATA = {
540 540 'extensions_largefiles': False,
541 541 'extensions_evolve': False,
542 542 'phases_publish': False
543 543 }
544 544
545 545 def test_creates_repo_hg_settings_when_data_is_correct(self, repo_stub):
546 546 model = VcsSettingsModel(repo=repo_stub.repo_name)
547 547 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
548 548 model.create_or_update_repo_hg_settings(self.FORM_DATA)
549 549 expected_calls = [
550 550 mock.call(model.repo_settings, 'extensions', 'largefiles', active=False, value=''),
551 551 mock.call(model.repo_settings, 'extensions', 'evolve', active=False, value=''),
552 552 mock.call(model.repo_settings, 'experimental', 'evolution', active=False, value=''),
553 553 mock.call(model.repo_settings, 'experimental', 'evolution.exchange', active=False, value='no'),
554 554 mock.call(model.repo_settings, 'extensions', 'topic', active=False, value=''),
555 555 mock.call(model.repo_settings, 'phases', 'publish', value='False'),
556 556 ]
557 557 assert expected_calls == create_mock.call_args_list
558 558
559 559 @pytest.mark.parametrize('field_to_remove', FORM_DATA.keys())
560 560 def test_key_is_not_found(self, repo_stub, field_to_remove):
561 561 model = VcsSettingsModel(repo=repo_stub.repo_name)
562 562 data = self.FORM_DATA.copy()
563 563 data.pop(field_to_remove)
564 564 with pytest.raises(ValueError) as exc_info:
565 565 model.create_or_update_repo_hg_settings(data)
566 566 Session().commit()
567 567
568 568 expected_message = 'The given data does not contain {} key'.format(
569 569 field_to_remove)
570 570 assert str(exc_info.value) == expected_message
571 571
572 572 def test_create_raises_exception_when_repository_not_specified(self):
573 573 model = VcsSettingsModel()
574 574 with pytest.raises(Exception) as exc_info:
575 575 model.create_or_update_repo_hg_settings(self.FORM_DATA)
576 576 Session().commit()
577 577
578 578 assert str(exc_info.value) == 'Repository is not specified'
579 579
580 580
581 class TestUpdateGlobalSslSetting(object):
582 def test_updates_global_hg_settings(self):
583 model = VcsSettingsModel()
584 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
585 model.update_global_ssl_setting('False')
586 Session().commit()
587
588 create_mock.assert_called_once_with(
589 model.global_settings, 'web', 'push_ssl', value='False')
590
591
592 581 class TestCreateOrUpdateGlobalHgSettings(object):
593 582 FORM_DATA = {
594 583 'extensions_largefiles': False,
595 'largefiles_usercache': '/example/largefiles-store',
596 584 'phases_publish': False,
597 585 'extensions_evolve': False
598 586 }
599 587
600 588 def test_creates_repo_hg_settings_when_data_is_correct(self):
601 589 model = VcsSettingsModel()
602 590 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
603 591 model.create_or_update_global_hg_settings(self.FORM_DATA)
604 592 Session().commit()
605 593
606 594 expected_calls = [
607 595 mock.call(model.global_settings, 'extensions', 'largefiles', active=False, value=''),
608 mock.call(model.global_settings, 'largefiles', 'usercache', value='/example/largefiles-store'),
609 596 mock.call(model.global_settings, 'phases', 'publish', value='False'),
610 597 mock.call(model.global_settings, 'extensions', 'evolve', active=False, value=''),
611 598 mock.call(model.global_settings, 'experimental', 'evolution', active=False, value=''),
612 599 mock.call(model.global_settings, 'experimental', 'evolution.exchange', active=False, value='no'),
613 600 mock.call(model.global_settings, 'extensions', 'topic', active=False, value=''),
614 601 ]
615 602
616 603 assert expected_calls == create_mock.call_args_list
617 604
618 605 @pytest.mark.parametrize('field_to_remove', FORM_DATA.keys())
619 606 def test_key_is_not_found(self, repo_stub, field_to_remove):
620 607 model = VcsSettingsModel(repo=repo_stub.repo_name)
621 608 data = self.FORM_DATA.copy()
622 609 data.pop(field_to_remove)
623 610 with pytest.raises(Exception) as exc_info:
624 611 model.create_or_update_global_hg_settings(data)
625 612 Session().commit()
626 613
627 614 expected_message = 'The given data does not contain {} key'.format(
628 615 field_to_remove)
629 616 assert str(exc_info.value) == expected_message
630 617
631 618
632 619 class TestCreateOrUpdateGlobalGitSettings(object):
633 620 FORM_DATA = {
634 621 'vcs_git_lfs_enabled': False,
635 'vcs_git_lfs_store_location': '/example/lfs-store',
636 622 }
637 623
638 624 def test_creates_repo_hg_settings_when_data_is_correct(self):
639 625 model = VcsSettingsModel()
640 626 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
641 627 model.create_or_update_global_git_settings(self.FORM_DATA)
642 628 Session().commit()
643 629
644 630 expected_calls = [
645 631 mock.call(model.global_settings, 'vcs_git_lfs', 'enabled', active=False, value=False),
646 mock.call(model.global_settings, 'vcs_git_lfs', 'store_location', value='/example/lfs-store'),
647 632 ]
648 633 assert expected_calls == create_mock.call_args_list
649 634
650 635
651 636 class TestDeleteRepoSvnPattern(object):
652 637 def test_success_when_repo_is_set(self, backend_svn, settings_util):
653 638 repo = backend_svn.create_repo()
654 639 repo_name = repo.repo_name
655 640
656 641 model = VcsSettingsModel(repo=repo_name)
657 642 entry = settings_util.create_repo_rhodecode_ui(
658 643 repo, VcsSettingsModel.SVN_BRANCH_SECTION, 'svn-branch')
659 644 Session().commit()
660 645
661 646 model.delete_repo_svn_pattern(entry.ui_id)
662 647
663 648 def test_fail_when_delete_id_from_other_repo(self, backend_svn):
664 649 repo_name = backend_svn.repo_name
665 650 model = VcsSettingsModel(repo=repo_name)
666 651 delete_ui_patch = mock.patch.object(model.repo_settings, 'delete_ui')
667 652 with delete_ui_patch as delete_ui_mock:
668 653 model.delete_repo_svn_pattern(123)
669 654 Session().commit()
670 655
671 656 delete_ui_mock.assert_called_once_with(-1)
672 657
673 658 def test_raises_exception_when_repository_is_not_specified(self):
674 659 model = VcsSettingsModel()
675 660 with pytest.raises(Exception) as exc_info:
676 661 model.delete_repo_svn_pattern(123)
677 662 assert str(exc_info.value) == 'Repository is not specified'
678 663
679 664
680 665 class TestDeleteGlobalSvnPattern(object):
681 666 def test_delete_global_svn_pattern_calls_delete_ui(self):
682 667 model = VcsSettingsModel()
683 668 delete_ui_patch = mock.patch.object(model.global_settings, 'delete_ui')
684 669 with delete_ui_patch as delete_ui_mock:
685 670 model.delete_global_svn_pattern(123)
686 671 delete_ui_mock.assert_called_once_with(123)
687 672
688 673
689 674 class TestFilterUiSettings(object):
690 675 def test_settings_are_filtered(self):
691 676 model = VcsSettingsModel()
692 677 repo_settings = [
693 678 UiSetting('extensions', 'largefiles', '', True),
694 679 UiSetting('phases', 'publish', 'True', True),
695 680 UiSetting('hooks', 'changegroup.repo_size', 'hook', True),
696 681 UiSetting('hooks', 'changegroup.push_logger', 'hook', True),
697 682 UiSetting('hooks', 'outgoing.pull_logger', 'hook', True),
698 683 UiSetting(
699 684 'vcs_svn_branch', '84223c972204fa545ca1b22dac7bef5b68d7442d',
700 685 'test_branch', True),
701 686 UiSetting(
702 687 'vcs_svn_tag', '84229c972204fa545ca1b22dac7bef5b68d7442d',
703 688 'test_tag', True),
704 689 ]
705 690 non_repo_settings = [
706 691 UiSetting('largefiles', 'usercache', '/example/largefiles-store', True),
707 692 UiSetting('test', 'outgoing.pull_logger', 'hook', True),
708 693 UiSetting('hooks', 'test2', 'hook', True),
709 694 UiSetting(
710 695 'vcs_svn_repo', '84229c972204fa545ca1b22dac7bef5b68d7442d',
711 696 'test_tag', True),
712 697 ]
713 698 settings = repo_settings + non_repo_settings
714 699 filtered_settings = model._filter_ui_settings(settings)
715 700 assert sorted(filtered_settings) == sorted(repo_settings)
716 701
717 702
718 703 class TestFilterGeneralSettings(object):
719 704 def test_settings_are_filtered(self):
720 705 model = VcsSettingsModel()
721 706 settings = {
722 707 'rhodecode_abcde': 'value1',
723 708 'rhodecode_vwxyz': 'value2',
724 709 }
725 710 general_settings = {
726 711 'rhodecode_{}'.format(key): 'value'
727 712 for key in VcsSettingsModel.GENERAL_SETTINGS
728 713 }
729 714 settings.update(general_settings)
730 715
731 716 filtered_settings = model._filter_general_settings(general_settings)
732 717 assert sorted(filtered_settings) == sorted(general_settings)
733 718
734 719
735 720 class TestGetRepoUiSettings(object):
736 721 def test_global_uis_are_returned_when_no_repo_uis_found(
737 722 self, repo_stub):
738 723 model = VcsSettingsModel(repo=repo_stub.repo_name)
739 724 result = model.get_repo_ui_settings()
740 725 svn_sections = (
741 726 VcsSettingsModel.SVN_TAG_SECTION,
742 727 VcsSettingsModel.SVN_BRANCH_SECTION)
743 728 expected_result = [
744 729 s for s in model.global_settings.get_ui()
745 730 if s.section not in svn_sections]
746 731 assert sorted(result) == sorted(expected_result)
747 732
748 733 def test_repo_uis_are_overriding_global_uis(
749 734 self, repo_stub, settings_util):
750 735 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
751 736 settings_util.create_repo_rhodecode_ui(
752 737 repo_stub, section, 'repo', key=key, active=False)
753 738 model = VcsSettingsModel(repo=repo_stub.repo_name)
754 739 result = model.get_repo_ui_settings()
755 740 for setting in result:
756 741 locator = (setting.section, setting.key)
757 742 if locator in VcsSettingsModel.HOOKS_SETTINGS:
758 743 assert setting.value == 'repo'
759 744
760 745 assert setting.active is False
761 746
762 747 def test_global_svn_patterns_are_not_in_list(
763 748 self, repo_stub, settings_util):
764 749 svn_sections = (
765 750 VcsSettingsModel.SVN_TAG_SECTION,
766 751 VcsSettingsModel.SVN_BRANCH_SECTION)
767 752 for section in svn_sections:
768 753 settings_util.create_rhodecode_ui(
769 754 section, 'repo', key='deadbeef' + section, active=False)
770 755 Session().commit()
771 756
772 757 model = VcsSettingsModel(repo=repo_stub.repo_name)
773 758 result = model.get_repo_ui_settings()
774 759 for setting in result:
775 760 assert setting.section not in svn_sections
776 761
777 762 def test_repo_uis_filtered_by_section_are_returned(
778 763 self, repo_stub, settings_util):
779 764 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
780 765 settings_util.create_repo_rhodecode_ui(
781 766 repo_stub, section, 'repo', key=key, active=False)
782 767 model = VcsSettingsModel(repo=repo_stub.repo_name)
783 768 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
784 769 result = model.get_repo_ui_settings(section=section)
785 770 for setting in result:
786 771 assert setting.section == section
787 772
788 773 def test_repo_uis_filtered_by_key_are_returned(
789 774 self, repo_stub, settings_util):
790 775 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
791 776 settings_util.create_repo_rhodecode_ui(
792 777 repo_stub, section, 'repo', key=key, active=False)
793 778 model = VcsSettingsModel(repo=repo_stub.repo_name)
794 779 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
795 780 result = model.get_repo_ui_settings(key=key)
796 781 for setting in result:
797 782 assert setting.key == key
798 783
799 784 def test_raises_exception_when_repository_is_not_specified(self):
800 785 model = VcsSettingsModel()
801 786 with pytest.raises(Exception) as exc_info:
802 787 model.get_repo_ui_settings()
803 788 assert str(exc_info.value) == 'Repository is not specified'
804 789
805 790
806 791 class TestGetRepoGeneralSettings(object):
807 792 def test_global_settings_are_returned_when_no_repo_settings_found(
808 793 self, repo_stub):
809 794 model = VcsSettingsModel(repo=repo_stub.repo_name)
810 795 result = model.get_repo_general_settings()
811 796 expected_result = model.global_settings.get_all_settings()
812 797 assert sorted(result) == sorted(expected_result)
813 798
814 799 def test_repo_uis_are_overriding_global_uis(
815 800 self, repo_stub, settings_util):
816 801 for key in VcsSettingsModel.GENERAL_SETTINGS:
817 802 settings_util.create_repo_rhodecode_setting(
818 803 repo_stub, key, 'abcde', type_='unicode')
819 804 Session().commit()
820 805
821 806 model = VcsSettingsModel(repo=repo_stub.repo_name)
822 807 result = model.get_repo_ui_settings()
823 808 for key in result:
824 809 if key in VcsSettingsModel.GENERAL_SETTINGS:
825 810 assert result[key] == 'abcde'
826 811
827 812 def test_raises_exception_when_repository_is_not_specified(self):
828 813 model = VcsSettingsModel()
829 814 with pytest.raises(Exception) as exc_info:
830 815 model.get_repo_general_settings()
831 816 assert str(exc_info.value) == 'Repository is not specified'
832 817
833 818
834 819 class TestGetGlobalGeneralSettings(object):
835 820 def test_global_settings_are_returned(self, repo_stub):
836 821 model = VcsSettingsModel()
837 822 result = model.get_global_general_settings()
838 823 expected_result = model.global_settings.get_all_settings()
839 824 assert sorted(result) == sorted(expected_result)
840 825
841 826 def test_repo_uis_are_not_overriding_global_uis(
842 827 self, repo_stub, settings_util):
843 828 for key in VcsSettingsModel.GENERAL_SETTINGS:
844 829 settings_util.create_repo_rhodecode_setting(
845 830 repo_stub, key, 'abcde', type_='unicode')
846 831 Session().commit()
847 832
848 833 model = VcsSettingsModel(repo=repo_stub.repo_name)
849 834 result = model.get_global_general_settings()
850 835 expected_result = model.global_settings.get_all_settings()
851 836 assert sorted(result) == sorted(expected_result)
852 837
853 838
854 839 class TestGetGlobalUiSettings(object):
855 840 def test_global_uis_are_returned(self, repo_stub):
856 841 model = VcsSettingsModel()
857 842 result = model.get_global_ui_settings()
858 843 expected_result = model.global_settings.get_ui()
859 844 assert sorted(result) == sorted(expected_result)
860 845
861 846 def test_repo_uis_are_not_overriding_global_uis(
862 847 self, repo_stub, settings_util):
863 848 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
864 849 settings_util.create_repo_rhodecode_ui(
865 850 repo_stub, section, 'repo', key=key, active=False)
866 851 Session().commit()
867 852
868 853 model = VcsSettingsModel(repo=repo_stub.repo_name)
869 854 result = model.get_global_ui_settings()
870 855 expected_result = model.global_settings.get_ui()
871 856 assert sorted(result) == sorted(expected_result)
872 857
873 858 def test_ui_settings_filtered_by_section(
874 859 self, repo_stub, settings_util):
875 860 model = VcsSettingsModel(repo=repo_stub.repo_name)
876 861 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
877 862 result = model.get_global_ui_settings(section=section)
878 863 expected_result = model.global_settings.get_ui(section=section)
879 864 assert sorted(result) == sorted(expected_result)
880 865
881 866 def test_ui_settings_filtered_by_key(
882 867 self, repo_stub, settings_util):
883 868 model = VcsSettingsModel(repo=repo_stub.repo_name)
884 869 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
885 870 result = model.get_global_ui_settings(key=key)
886 871 expected_result = model.global_settings.get_ui(key=key)
887 872 assert sorted(result) == sorted(expected_result)
888 873
889 874
890 875 class TestGetGeneralSettings(object):
891 876 def test_global_settings_are_returned_when_inherited_is_true(
892 877 self, repo_stub, settings_util):
893 878 model = VcsSettingsModel(repo=repo_stub.repo_name)
894 879 model.inherit_global_settings = True
895 880 for key in VcsSettingsModel.GENERAL_SETTINGS:
896 881 settings_util.create_repo_rhodecode_setting(
897 882 repo_stub, key, 'abcde', type_='unicode')
898 883 Session().commit()
899 884
900 885 result = model.get_general_settings()
901 886 expected_result = model.get_global_general_settings()
902 887 assert sorted(result) == sorted(expected_result)
903 888
904 889 def test_repo_settings_are_returned_when_inherited_is_false(
905 890 self, repo_stub, settings_util):
906 891 model = VcsSettingsModel(repo=repo_stub.repo_name)
907 892 model.inherit_global_settings = False
908 893 for key in VcsSettingsModel.GENERAL_SETTINGS:
909 894 settings_util.create_repo_rhodecode_setting(
910 895 repo_stub, key, 'abcde', type_='unicode')
911 896 Session().commit()
912 897
913 898 result = model.get_general_settings()
914 899 expected_result = model.get_repo_general_settings()
915 900 assert sorted(result) == sorted(expected_result)
916 901
917 902 def test_global_settings_are_returned_when_no_repository_specified(self):
918 903 model = VcsSettingsModel()
919 904 result = model.get_general_settings()
920 905 expected_result = model.get_global_general_settings()
921 906 assert sorted(result) == sorted(expected_result)
922 907
923 908
924 909 class TestGetUiSettings(object):
925 910 def test_global_settings_are_returned_when_inherited_is_true(
926 911 self, repo_stub, settings_util):
927 912 model = VcsSettingsModel(repo=repo_stub.repo_name)
928 913 model.inherit_global_settings = True
929 914 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
930 915 settings_util.create_repo_rhodecode_ui(
931 916 repo_stub, section, 'repo', key=key, active=True)
932 917 Session().commit()
933 918
934 919 result = model.get_ui_settings()
935 920 expected_result = model.get_global_ui_settings()
936 921 assert sorted(result) == sorted(expected_result)
937 922
938 923 def test_repo_settings_are_returned_when_inherited_is_false(
939 924 self, repo_stub, settings_util):
940 925 model = VcsSettingsModel(repo=repo_stub.repo_name)
941 926 model.inherit_global_settings = False
942 927 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
943 928 settings_util.create_repo_rhodecode_ui(
944 929 repo_stub, section, 'repo', key=key, active=True)
945 930 Session().commit()
946 931
947 932 result = model.get_ui_settings()
948 933 expected_result = model.get_repo_ui_settings()
949 934 assert sorted(result) == sorted(expected_result)
950 935
951 936 def test_repo_settings_filtered_by_section_and_key(self, repo_stub):
952 937 model = VcsSettingsModel(repo=repo_stub.repo_name)
953 938 model.inherit_global_settings = False
954 939
955 940 args = ('section', 'key')
956 941 with mock.patch.object(model, 'get_repo_ui_settings') as settings_mock:
957 942 model.get_ui_settings(*args)
958 943 Session().commit()
959 944
960 945 settings_mock.assert_called_once_with(*args)
961 946
962 947 def test_global_settings_filtered_by_section_and_key(self):
963 948 model = VcsSettingsModel()
964 949 args = ('section', 'key')
965 950 with mock.patch.object(model, 'get_global_ui_settings') as (
966 951 settings_mock):
967 952 model.get_ui_settings(*args)
968 953 settings_mock.assert_called_once_with(*args)
969 954
970 955 def test_global_settings_are_returned_when_no_repository_specified(self):
971 956 model = VcsSettingsModel()
972 957 result = model.get_ui_settings()
973 958 expected_result = model.get_global_ui_settings()
974 959 assert sorted(result) == sorted(expected_result)
975 960
976 961
977 962 class TestGetSvnPatterns(object):
978 963 def test_repo_settings_filtered_by_section_and_key(self, repo_stub):
979 964 model = VcsSettingsModel(repo=repo_stub.repo_name)
980 965 args = ('section', )
981 966 with mock.patch.object(model, 'get_repo_ui_settings') as settings_mock:
982 967 model.get_svn_patterns(*args)
983 968
984 969 Session().commit()
985 970 settings_mock.assert_called_once_with(*args)
986 971
987 972 def test_global_settings_filtered_by_section_and_key(self):
988 973 model = VcsSettingsModel()
989 974 args = ('section', )
990 975 with mock.patch.object(model, 'get_global_ui_settings') as (
991 976 settings_mock):
992 977 model.get_svn_patterns(*args)
993 978 settings_mock.assert_called_once_with(*args)
994 979
995 980
996 981 class TestCreateOrUpdateRepoSettings(object):
997 982 FORM_DATA = {
998 983 'inherit_global_settings': False,
999 984 'hooks_changegroup_repo_size': False,
1000 985 'hooks_changegroup_push_logger': False,
1001 986 'hooks_outgoing_pull_logger': False,
1002 987 'extensions_largefiles': False,
1003 988 'extensions_evolve': False,
1004 'largefiles_usercache': '/example/largefiles-store',
1005 989 'vcs_git_lfs_enabled': False,
1006 'vcs_git_lfs_store_location': '/',
1007 990 'phases_publish': 'False',
1008 991 'rhodecode_pr_merge_enabled': False,
1009 992 'rhodecode_use_outdated_comments': False,
1010 993 'new_svn_branch': '',
1011 994 'new_svn_tag': ''
1012 995 }
1013 996
1014 997 def test_get_raises_exception_when_repository_not_specified(self):
1015 998 model = VcsSettingsModel()
1016 999 with pytest.raises(Exception) as exc_info:
1017 1000 model.create_or_update_repo_settings(data=self.FORM_DATA)
1018 1001 Session().commit()
1019 1002
1020 1003 assert str(exc_info.value) == 'Repository is not specified'
1021 1004
1022 1005 def test_only_svn_settings_are_updated_when_type_is_svn(self, backend_svn):
1023 1006 repo = backend_svn.create_repo()
1024 1007 model = VcsSettingsModel(repo=repo)
1025 1008 with self._patch_model(model) as mocks:
1026 1009 model.create_or_update_repo_settings(
1027 1010 data=self.FORM_DATA, inherit_global_settings=False)
1028 1011 Session().commit()
1029 1012
1030 1013 mocks['create_repo_svn_settings'].assert_called_once_with(
1031 1014 self.FORM_DATA)
1032 1015 non_called_methods = (
1033 1016 'create_or_update_repo_hook_settings',
1034 1017 'create_or_update_repo_pr_settings',
1035 1018 'create_or_update_repo_hg_settings')
1036 1019 for method in non_called_methods:
1037 1020 assert mocks[method].call_count == 0
1038 1021
1039 1022 def test_non_svn_settings_are_updated_when_type_is_hg(self, backend_hg):
1040 1023 repo = backend_hg.create_repo()
1041 1024 model = VcsSettingsModel(repo=repo)
1042 1025 with self._patch_model(model) as mocks:
1043 1026 model.create_or_update_repo_settings(
1044 1027 data=self.FORM_DATA, inherit_global_settings=False)
1045 1028 Session().commit()
1046 1029
1047 1030 assert mocks['create_repo_svn_settings'].call_count == 0
1048 1031 called_methods = (
1049 1032 'create_or_update_repo_hook_settings',
1050 1033 'create_or_update_repo_pr_settings',
1051 1034 'create_or_update_repo_hg_settings')
1052 1035 for method in called_methods:
1053 1036 mocks[method].assert_called_once_with(self.FORM_DATA)
1054 1037
1055 1038 def test_non_svn_and_hg_settings_are_updated_when_type_is_git(
1056 1039 self, backend_git):
1057 1040 repo = backend_git.create_repo()
1058 1041 model = VcsSettingsModel(repo=repo)
1059 1042 with self._patch_model(model) as mocks:
1060 1043 model.create_or_update_repo_settings(
1061 1044 data=self.FORM_DATA, inherit_global_settings=False)
1062 1045
1063 1046 assert mocks['create_repo_svn_settings'].call_count == 0
1064 1047 called_methods = (
1065 1048 'create_or_update_repo_hook_settings',
1066 1049 'create_or_update_repo_pr_settings')
1067 1050 non_called_methods = (
1068 1051 'create_repo_svn_settings',
1069 1052 'create_or_update_repo_hg_settings'
1070 1053 )
1071 1054 for method in called_methods:
1072 1055 mocks[method].assert_called_once_with(self.FORM_DATA)
1073 1056 for method in non_called_methods:
1074 1057 assert mocks[method].call_count == 0
1075 1058
1076 1059 def test_no_methods_are_called_when_settings_are_inherited(
1077 1060 self, backend):
1078 1061 repo = backend.create_repo()
1079 1062 model = VcsSettingsModel(repo=repo)
1080 1063 with self._patch_model(model) as mocks:
1081 1064 model.create_or_update_repo_settings(
1082 1065 data=self.FORM_DATA, inherit_global_settings=True)
1083 1066 for method_name in mocks:
1084 1067 assert mocks[method_name].call_count == 0
1085 1068
1086 1069 def test_cache_is_marked_for_invalidation(self, repo_stub):
1087 1070 model = VcsSettingsModel(repo=repo_stub)
1088 1071 invalidation_patcher = mock.patch(
1089 1072 'rhodecode.model.scm.ScmModel.mark_for_invalidation')
1090 1073 with invalidation_patcher as invalidation_mock:
1091 1074 model.create_or_update_repo_settings(
1092 1075 data=self.FORM_DATA, inherit_global_settings=True)
1093 1076 Session().commit()
1094 1077
1095 1078 invalidation_mock.assert_called_once_with(
1096 1079 repo_stub.repo_name, delete=True)
1097 1080
1098 1081 def test_inherit_flag_is_saved(self, repo_stub):
1099 1082 model = VcsSettingsModel(repo=repo_stub)
1100 1083 model.inherit_global_settings = True
1101 1084 with self._patch_model(model):
1102 1085 model.create_or_update_repo_settings(
1103 1086 data=self.FORM_DATA, inherit_global_settings=False)
1104 1087 Session().commit()
1105 1088
1106 1089 assert model.inherit_global_settings is False
1107 1090
1108 1091 def _patch_model(self, model):
1109 1092 return mock.patch.multiple(
1110 1093 model,
1111 1094 create_repo_svn_settings=mock.DEFAULT,
1112 1095 create_or_update_repo_hook_settings=mock.DEFAULT,
1113 1096 create_or_update_repo_pr_settings=mock.DEFAULT,
1114 1097 create_or_update_repo_hg_settings=mock.DEFAULT)
@@ -1,978 +1,978 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import mock
21 21 import pytest
22 22 import textwrap
23 23
24 24 import rhodecode
25 25 from rhodecode.lib.vcs.backends import get_backend
26 26 from rhodecode.lib.vcs.backends.base import (
27 27 MergeResponse, MergeFailureReason, Reference)
28 28 from rhodecode.lib.vcs.exceptions import RepositoryError
29 29 from rhodecode.lib.vcs.nodes import FileNode
30 30 from rhodecode.model.comment import CommentsModel
31 31 from rhodecode.model.db import PullRequest, Session
32 32 from rhodecode.model.pull_request import PullRequestModel
33 33 from rhodecode.model.user import UserModel
34 34 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
35 35 from rhodecode.lib.str_utils import safe_str
36 36
37 37 pytestmark = [
38 38 pytest.mark.backends("git", "hg"),
39 39 ]
40 40
41 41
42 42 @pytest.mark.usefixtures('config_stub')
43 43 class TestPullRequestModel(object):
44 44
45 45 @pytest.fixture()
46 46 def pull_request(self, request, backend, pr_util):
47 47 """
48 48 A pull request combined with multiples patches.
49 49 """
50 50 BackendClass = get_backend(backend.alias)
51 51 merge_resp = MergeResponse(
52 52 False, False, None, MergeFailureReason.UNKNOWN,
53 53 metadata={'exception': 'MockError'})
54 54 self.merge_patcher = mock.patch.object(
55 55 BackendClass, 'merge', return_value=merge_resp)
56 56 self.workspace_remove_patcher = mock.patch.object(
57 57 BackendClass, 'cleanup_merge_workspace')
58 58
59 59 self.workspace_remove_mock = self.workspace_remove_patcher.start()
60 60 self.merge_mock = self.merge_patcher.start()
61 61 self.comment_patcher = mock.patch(
62 62 'rhodecode.model.changeset_status.ChangesetStatusModel.set_status')
63 63 self.comment_patcher.start()
64 64 self.notification_patcher = mock.patch(
65 65 'rhodecode.model.notification.NotificationModel.create')
66 66 self.notification_patcher.start()
67 67 self.helper_patcher = mock.patch(
68 68 'rhodecode.lib.helpers.route_path')
69 69 self.helper_patcher.start()
70 70
71 71 self.hook_patcher = mock.patch.object(PullRequestModel,
72 72 'trigger_pull_request_hook')
73 73 self.hook_mock = self.hook_patcher.start()
74 74
75 75 self.invalidation_patcher = mock.patch(
76 76 'rhodecode.model.pull_request.ScmModel.mark_for_invalidation')
77 77 self.invalidation_mock = self.invalidation_patcher.start()
78 78
79 79 self.pull_request = pr_util.create_pull_request(
80 80 mergeable=True, name_suffix=u'ąć')
81 81 self.source_commit = self.pull_request.source_ref_parts.commit_id
82 82 self.target_commit = self.pull_request.target_ref_parts.commit_id
83 83 self.workspace_id = 'pr-%s' % self.pull_request.pull_request_id
84 84 self.repo_id = self.pull_request.target_repo.repo_id
85 85
86 86 @request.addfinalizer
87 87 def cleanup_pull_request():
88 88 calls = [mock.call(
89 89 self.pull_request, self.pull_request.author, 'create')]
90 90 self.hook_mock.assert_has_calls(calls)
91 91
92 92 self.workspace_remove_patcher.stop()
93 93 self.merge_patcher.stop()
94 94 self.comment_patcher.stop()
95 95 self.notification_patcher.stop()
96 96 self.helper_patcher.stop()
97 97 self.hook_patcher.stop()
98 98 self.invalidation_patcher.stop()
99 99
100 100 return self.pull_request
101 101
102 102 def test_get_all(self, pull_request):
103 103 prs = PullRequestModel().get_all(pull_request.target_repo)
104 104 assert isinstance(prs, list)
105 105 assert len(prs) == 1
106 106
107 107 def test_count_all(self, pull_request):
108 108 pr_count = PullRequestModel().count_all(pull_request.target_repo)
109 109 assert pr_count == 1
110 110
111 111 def test_get_awaiting_review(self, pull_request):
112 112 prs = PullRequestModel().get_awaiting_review(pull_request.target_repo)
113 113 assert isinstance(prs, list)
114 114 assert len(prs) == 1
115 115
116 116 def test_count_awaiting_review(self, pull_request):
117 117 pr_count = PullRequestModel().count_awaiting_review(
118 118 pull_request.target_repo)
119 119 assert pr_count == 1
120 120
121 121 def test_get_awaiting_my_review(self, pull_request):
122 122 PullRequestModel().update_reviewers(
123 123 pull_request, [(pull_request.author, ['author'], False, 'reviewer', [])],
124 124 pull_request.author)
125 125 Session().commit()
126 126
127 127 prs = PullRequestModel().get_awaiting_my_review(
128 128 pull_request.target_repo.repo_name, user_id=pull_request.author.user_id)
129 129 assert isinstance(prs, list)
130 130 assert len(prs) == 1
131 131
132 132 def test_count_awaiting_my_review(self, pull_request):
133 133 PullRequestModel().update_reviewers(
134 134 pull_request, [(pull_request.author, ['author'], False, 'reviewer', [])],
135 135 pull_request.author)
136 136 Session().commit()
137 137
138 138 pr_count = PullRequestModel().count_awaiting_my_review(
139 139 pull_request.target_repo.repo_name, user_id=pull_request.author.user_id)
140 140 assert pr_count == 1
141 141
142 142 def test_delete_calls_cleanup_merge(self, pull_request):
143 143 repo_id = pull_request.target_repo.repo_id
144 144 PullRequestModel().delete(pull_request, pull_request.author)
145 145 Session().commit()
146 146
147 147 self.workspace_remove_mock.assert_called_once_with(
148 148 repo_id, self.workspace_id)
149 149
150 150 def test_close_calls_cleanup_and_hook(self, pull_request):
151 151 PullRequestModel().close_pull_request(
152 152 pull_request, pull_request.author)
153 153 Session().commit()
154 154
155 155 repo_id = pull_request.target_repo.repo_id
156 156
157 157 self.workspace_remove_mock.assert_called_once_with(
158 158 repo_id, self.workspace_id)
159 159 self.hook_mock.assert_called_with(
160 160 self.pull_request, self.pull_request.author, 'close')
161 161
162 162 def test_merge_status(self, pull_request):
163 163 self.merge_mock.return_value = MergeResponse(
164 164 True, False, None, MergeFailureReason.NONE)
165 165
166 166 assert pull_request._last_merge_source_rev is None
167 167 assert pull_request._last_merge_target_rev is None
168 168 assert pull_request.last_merge_status is None
169 169
170 170 merge_response, status, msg = PullRequestModel().merge_status(pull_request)
171 171 assert status is True
172 172 assert msg == 'This pull request can be automatically merged.'
173 173 self.merge_mock.assert_called_with(
174 174 self.repo_id, self.workspace_id,
175 175 pull_request.target_ref_parts,
176 176 pull_request.source_repo.scm_instance(),
177 177 pull_request.source_ref_parts, dry_run=True,
178 178 use_rebase=False, close_branch=False)
179 179
180 180 assert pull_request._last_merge_source_rev == self.source_commit
181 181 assert pull_request._last_merge_target_rev == self.target_commit
182 182 assert pull_request.last_merge_status is MergeFailureReason.NONE
183 183
184 184 self.merge_mock.reset_mock()
185 185 merge_response, status, msg = PullRequestModel().merge_status(pull_request)
186 186 assert status is True
187 187 assert msg == 'This pull request can be automatically merged.'
188 188 assert self.merge_mock.called is False
189 189
190 190 def test_merge_status_known_failure(self, pull_request):
191 191 self.merge_mock.return_value = MergeResponse(
192 192 False, False, None, MergeFailureReason.MERGE_FAILED,
193 193 metadata={'unresolved_files': 'file1'})
194 194
195 195 assert pull_request._last_merge_source_rev is None
196 196 assert pull_request._last_merge_target_rev is None
197 197 assert pull_request.last_merge_status is None
198 198
199 199 merge_response, status, msg = PullRequestModel().merge_status(pull_request)
200 200 assert status is False
201 201 assert msg == 'This pull request cannot be merged because of merge conflicts. file1'
202 202 self.merge_mock.assert_called_with(
203 203 self.repo_id, self.workspace_id,
204 204 pull_request.target_ref_parts,
205 205 pull_request.source_repo.scm_instance(),
206 206 pull_request.source_ref_parts, dry_run=True,
207 207 use_rebase=False, close_branch=False)
208 208
209 209 assert pull_request._last_merge_source_rev == self.source_commit
210 210 assert pull_request._last_merge_target_rev == self.target_commit
211 211 assert pull_request.last_merge_status is MergeFailureReason.MERGE_FAILED
212 212
213 213 self.merge_mock.reset_mock()
214 214 merge_response, status, msg = PullRequestModel().merge_status(pull_request)
215 215 assert status is False
216 216 assert msg == 'This pull request cannot be merged because of merge conflicts. file1'
217 217 assert self.merge_mock.called is False
218 218
219 219 def test_merge_status_unknown_failure(self, pull_request):
220 220 self.merge_mock.return_value = MergeResponse(
221 221 False, False, None, MergeFailureReason.UNKNOWN,
222 222 metadata={'exception': 'MockError'})
223 223
224 224 assert pull_request._last_merge_source_rev is None
225 225 assert pull_request._last_merge_target_rev is None
226 226 assert pull_request.last_merge_status is None
227 227
228 228 merge_response, status, msg = PullRequestModel().merge_status(pull_request)
229 229 assert status is False
230 230 assert msg == (
231 231 'This pull request cannot be merged because of an unhandled exception. '
232 232 'MockError')
233 233 self.merge_mock.assert_called_with(
234 234 self.repo_id, self.workspace_id,
235 235 pull_request.target_ref_parts,
236 236 pull_request.source_repo.scm_instance(),
237 237 pull_request.source_ref_parts, dry_run=True,
238 238 use_rebase=False, close_branch=False)
239 239
240 240 assert pull_request._last_merge_source_rev is None
241 241 assert pull_request._last_merge_target_rev is None
242 242 assert pull_request.last_merge_status is None
243 243
244 244 self.merge_mock.reset_mock()
245 245 merge_response, status, msg = PullRequestModel().merge_status(pull_request)
246 246 assert status is False
247 247 assert msg == (
248 248 'This pull request cannot be merged because of an unhandled exception. '
249 249 'MockError')
250 250 assert self.merge_mock.called is True
251 251
252 252 def test_merge_status_when_target_is_locked(self, pull_request):
253 253 pull_request.target_repo.locked = [1, u'12345.50', 'lock_web']
254 254 merge_response, status, msg = PullRequestModel().merge_status(pull_request)
255 255 assert status is False
256 256 assert msg == (
257 257 'This pull request cannot be merged because the target repository '
258 258 'is locked by user:1.')
259 259
260 260 def test_merge_status_requirements_check_target(self, pull_request):
261 261
262 262 def has_largefiles(self, repo):
263 263 return repo == pull_request.source_repo
264 264
265 265 patcher = mock.patch.object(PullRequestModel, '_has_largefiles', has_largefiles)
266 266 with patcher:
267 267 merge_response, status, msg = PullRequestModel().merge_status(pull_request)
268 268
269 269 assert status is False
270 270 assert msg == 'Target repository large files support is disabled.'
271 271
272 272 def test_merge_status_requirements_check_source(self, pull_request):
273 273
274 274 def has_largefiles(self, repo):
275 275 return repo == pull_request.target_repo
276 276
277 277 patcher = mock.patch.object(PullRequestModel, '_has_largefiles', has_largefiles)
278 278 with patcher:
279 279 merge_response, status, msg = PullRequestModel().merge_status(pull_request)
280 280
281 281 assert status is False
282 282 assert msg == 'Source repository large files support is disabled.'
283 283
284 284 def test_merge(self, pull_request, merge_extras):
285 285 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
286 286 merge_ref = Reference(
287 287 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
288 288 self.merge_mock.return_value = MergeResponse(
289 289 True, True, merge_ref, MergeFailureReason.NONE)
290 290
291 291 merge_extras['repository'] = pull_request.target_repo.repo_name
292 292 PullRequestModel().merge_repo(
293 293 pull_request, pull_request.author, extras=merge_extras)
294 294 Session().commit()
295 295
296 296 message = (
297 297 u'Merge pull request !{pr_id} from {source_repo} {source_ref_name}'
298 298 u'\n\n {pr_title}'.format(
299 299 pr_id=pull_request.pull_request_id,
300 300 source_repo=safe_str(
301 301 pull_request.source_repo.scm_instance().name),
302 302 source_ref_name=pull_request.source_ref_parts.name,
303 303 pr_title=safe_str(pull_request.title)
304 304 )
305 305 )
306 306 self.merge_mock.assert_called_with(
307 307 self.repo_id, self.workspace_id,
308 308 pull_request.target_ref_parts,
309 309 pull_request.source_repo.scm_instance(),
310 310 pull_request.source_ref_parts,
311 311 user_name=user.short_contact, user_email=user.email, message=message,
312 312 use_rebase=False, close_branch=False
313 313 )
314 314 self.invalidation_mock.assert_called_once_with(
315 315 pull_request.target_repo.repo_name)
316 316
317 317 self.hook_mock.assert_called_with(
318 318 self.pull_request, self.pull_request.author, 'merge')
319 319
320 320 pull_request = PullRequest.get(pull_request.pull_request_id)
321 321 assert pull_request.merge_rev == '6126b7bfcc82ad2d3deaee22af926b082ce54cc6'
322 322
323 323 def test_merge_with_status_lock(self, pull_request, merge_extras):
324 324 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
325 325 merge_ref = Reference(
326 326 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
327 327 self.merge_mock.return_value = MergeResponse(
328 328 True, True, merge_ref, MergeFailureReason.NONE)
329 329
330 330 merge_extras['repository'] = pull_request.target_repo.repo_name
331 331
332 332 with pull_request.set_state(PullRequest.STATE_UPDATING):
333 333 assert pull_request.pull_request_state == PullRequest.STATE_UPDATING
334 334 PullRequestModel().merge_repo(
335 335 pull_request, pull_request.author, extras=merge_extras)
336 336 Session().commit()
337 337
338 338 assert pull_request.pull_request_state == PullRequest.STATE_CREATED
339 339
340 340 message = (
341 341 u'Merge pull request !{pr_id} from {source_repo} {source_ref_name}'
342 342 u'\n\n {pr_title}'.format(
343 343 pr_id=pull_request.pull_request_id,
344 344 source_repo=safe_str(
345 345 pull_request.source_repo.scm_instance().name),
346 346 source_ref_name=pull_request.source_ref_parts.name,
347 347 pr_title=safe_str(pull_request.title)
348 348 )
349 349 )
350 350 self.merge_mock.assert_called_with(
351 351 self.repo_id, self.workspace_id,
352 352 pull_request.target_ref_parts,
353 353 pull_request.source_repo.scm_instance(),
354 354 pull_request.source_ref_parts,
355 355 user_name=user.short_contact, user_email=user.email, message=message,
356 356 use_rebase=False, close_branch=False
357 357 )
358 358 self.invalidation_mock.assert_called_once_with(
359 359 pull_request.target_repo.repo_name)
360 360
361 361 self.hook_mock.assert_called_with(
362 362 self.pull_request, self.pull_request.author, 'merge')
363 363
364 364 pull_request = PullRequest.get(pull_request.pull_request_id)
365 365 assert pull_request.merge_rev == '6126b7bfcc82ad2d3deaee22af926b082ce54cc6'
366 366
367 367 def test_merge_failed(self, pull_request, merge_extras):
368 368 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
369 369 merge_ref = Reference(
370 370 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
371 371 self.merge_mock.return_value = MergeResponse(
372 372 False, False, merge_ref, MergeFailureReason.MERGE_FAILED)
373 373
374 374 merge_extras['repository'] = pull_request.target_repo.repo_name
375 375 PullRequestModel().merge_repo(
376 376 pull_request, pull_request.author, extras=merge_extras)
377 377 Session().commit()
378 378
379 379 message = (
380 380 u'Merge pull request !{pr_id} from {source_repo} {source_ref_name}'
381 381 u'\n\n {pr_title}'.format(
382 382 pr_id=pull_request.pull_request_id,
383 383 source_repo=safe_str(
384 384 pull_request.source_repo.scm_instance().name),
385 385 source_ref_name=pull_request.source_ref_parts.name,
386 386 pr_title=safe_str(pull_request.title)
387 387 )
388 388 )
389 389 self.merge_mock.assert_called_with(
390 390 self.repo_id, self.workspace_id,
391 391 pull_request.target_ref_parts,
392 392 pull_request.source_repo.scm_instance(),
393 393 pull_request.source_ref_parts,
394 394 user_name=user.short_contact, user_email=user.email, message=message,
395 395 use_rebase=False, close_branch=False
396 396 )
397 397
398 398 pull_request = PullRequest.get(pull_request.pull_request_id)
399 399 assert self.invalidation_mock.called is False
400 400 assert pull_request.merge_rev is None
401 401
402 402 def test_get_commit_ids(self, pull_request):
403 403 # The PR has been not merged yet, so expect an exception
404 404 with pytest.raises(ValueError):
405 405 PullRequestModel()._get_commit_ids(pull_request)
406 406
407 407 # Merge revision is in the revisions list
408 408 pull_request.merge_rev = pull_request.revisions[0]
409 409 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
410 410 assert commit_ids == pull_request.revisions
411 411
412 412 # Merge revision is not in the revisions list
413 413 pull_request.merge_rev = 'f000' * 10
414 414 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
415 415 assert commit_ids == pull_request.revisions + [pull_request.merge_rev]
416 416
417 417 def test_get_diff_from_pr_version(self, pull_request):
418 418 source_repo = pull_request.source_repo
419 419 source_ref_id = pull_request.source_ref_parts.commit_id
420 420 target_ref_id = pull_request.target_ref_parts.commit_id
421 421 diff = PullRequestModel()._get_diff_from_pr_or_version(
422 422 source_repo, source_ref_id, target_ref_id,
423 423 hide_whitespace_changes=False, diff_context=6)
424 424 assert b'file_1' in diff.raw.tobytes()
425 425
426 426 def test_generate_title_returns_unicode(self):
427 427 title = PullRequestModel().generate_pullrequest_title(
428 428 source='source-dummy',
429 429 source_ref='source-ref-dummy',
430 430 target='target-dummy',
431 431 )
432 432 assert type(title) == str
433 433
434 434 @pytest.mark.parametrize('title, has_wip', [
435 435 ('hello', False),
436 436 ('hello wip', False),
437 437 ('hello wip: xxx', False),
438 438 ('[wip] hello', True),
439 439 ('[wip] hello', True),
440 440 ('wip: hello', True),
441 441 ('wip hello', True),
442 442
443 443 ])
444 444 def test_wip_title_marker(self, pull_request, title, has_wip):
445 445 pull_request.title = title
446 446 assert pull_request.work_in_progress == has_wip
447 447
448 448
449 449 @pytest.mark.usefixtures('config_stub')
450 450 class TestIntegrationMerge(object):
451 451 @pytest.mark.parametrize('extra_config', (
452 {'vcs.hooks.protocol': 'http', 'vcs.hooks.direct_calls': False},
452 {'vcs.hooks.protocol.v2': 'celery', 'vcs.hooks.direct_calls': False},
453 453 ))
454 454 def test_merge_triggers_push_hooks(
455 455 self, pr_util, user_admin, capture_rcextensions, merge_extras,
456 456 extra_config):
457 457
458 458 pull_request = pr_util.create_pull_request(
459 459 approved=True, mergeable=True)
460 460 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
461 461 merge_extras['repository'] = pull_request.target_repo.repo_name
462 462 Session().commit()
463 463
464 464 with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False):
465 465 merge_state = PullRequestModel().merge_repo(
466 466 pull_request, user_admin, extras=merge_extras)
467 467 Session().commit()
468 468
469 469 assert merge_state.executed
470 470 assert '_pre_push_hook' in capture_rcextensions
471 471 assert '_push_hook' in capture_rcextensions
472 472
473 473 def test_merge_can_be_rejected_by_pre_push_hook(
474 474 self, pr_util, user_admin, capture_rcextensions, merge_extras):
475 475 pull_request = pr_util.create_pull_request(
476 476 approved=True, mergeable=True)
477 477 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
478 478 merge_extras['repository'] = pull_request.target_repo.repo_name
479 479 Session().commit()
480 480
481 481 with mock.patch('rhodecode.EXTENSIONS.PRE_PUSH_HOOK') as pre_pull:
482 482 pre_pull.side_effect = RepositoryError("Disallow push!")
483 483 merge_status = PullRequestModel().merge_repo(
484 484 pull_request, user_admin, extras=merge_extras)
485 485 Session().commit()
486 486
487 487 assert not merge_status.executed
488 488 assert 'pre_push' not in capture_rcextensions
489 489 assert 'post_push' not in capture_rcextensions
490 490
491 491 def test_merge_fails_if_target_is_locked(
492 492 self, pr_util, user_regular, merge_extras):
493 493 pull_request = pr_util.create_pull_request(
494 494 approved=True, mergeable=True)
495 495 locked_by = [user_regular.user_id + 1, 12345.50, 'lock_web']
496 496 pull_request.target_repo.locked = locked_by
497 497 # TODO: johbo: Check if this can work based on the database, currently
498 498 # all data is pre-computed, that's why just updating the DB is not
499 499 # enough.
500 500 merge_extras['locked_by'] = locked_by
501 501 merge_extras['repository'] = pull_request.target_repo.repo_name
502 502 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
503 503 Session().commit()
504 504 merge_status = PullRequestModel().merge_repo(
505 505 pull_request, user_regular, extras=merge_extras)
506 506 Session().commit()
507 507
508 508 assert not merge_status.executed
509 509
510 510
511 511 @pytest.mark.parametrize('use_outdated, inlines_count, outdated_count', [
512 512 (False, 1, 0),
513 513 (True, 0, 1),
514 514 ])
515 515 def test_outdated_comments(
516 516 pr_util, use_outdated, inlines_count, outdated_count, config_stub):
517 517 pull_request = pr_util.create_pull_request()
518 518 pr_util.create_inline_comment(file_path='not_in_updated_diff')
519 519
520 520 with outdated_comments_patcher(use_outdated) as outdated_comment_mock:
521 521 pr_util.add_one_commit()
522 522 assert_inline_comments(
523 523 pull_request, visible=inlines_count, outdated=outdated_count)
524 524 outdated_comment_mock.assert_called_with(pull_request)
525 525
526 526
527 527 @pytest.mark.parametrize('mr_type, expected_msg', [
528 528 (MergeFailureReason.NONE,
529 529 'This pull request can be automatically merged.'),
530 530 (MergeFailureReason.UNKNOWN,
531 531 'This pull request cannot be merged because of an unhandled exception. CRASH'),
532 532 (MergeFailureReason.MERGE_FAILED,
533 533 'This pull request cannot be merged because of merge conflicts. CONFLICT_FILE'),
534 534 (MergeFailureReason.PUSH_FAILED,
535 535 'This pull request could not be merged because push to target:`some-repo@merge_commit` failed.'),
536 536 (MergeFailureReason.TARGET_IS_NOT_HEAD,
537 537 'This pull request cannot be merged because the target `ref_name` is not a head.'),
538 538 (MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES,
539 539 'This pull request cannot be merged because the source contains more branches than the target.'),
540 540 (MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS,
541 541 'This pull request cannot be merged because the target `ref_name` has multiple heads: `a,b,c`.'),
542 542 (MergeFailureReason.TARGET_IS_LOCKED,
543 543 'This pull request cannot be merged because the target repository is locked by user:123.'),
544 544 (MergeFailureReason.MISSING_TARGET_REF,
545 545 'This pull request cannot be merged because the target reference `ref_name` is missing.'),
546 546 (MergeFailureReason.MISSING_SOURCE_REF,
547 547 'This pull request cannot be merged because the source reference `ref_name` is missing.'),
548 548 (MergeFailureReason.SUBREPO_MERGE_FAILED,
549 549 'This pull request cannot be merged because of conflicts related to sub repositories.'),
550 550
551 551 ])
552 552 def test_merge_response_message(mr_type, expected_msg):
553 553 merge_ref = Reference('type', 'ref_name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
554 554 metadata = {
555 555 'unresolved_files': 'CONFLICT_FILE',
556 556 'exception': "CRASH",
557 557 'target': 'some-repo',
558 558 'merge_commit': 'merge_commit',
559 559 'target_ref': merge_ref,
560 560 'source_ref': merge_ref,
561 561 'heads': ','.join(['a', 'b', 'c']),
562 562 'locked_by': 'user:123'
563 563 }
564 564
565 565 merge_response = MergeResponse(True, True, merge_ref, mr_type, metadata=metadata)
566 566 assert merge_response.merge_status_message == expected_msg
567 567
568 568
569 569 @pytest.fixture()
570 570 def merge_extras(user_regular):
571 571 """
572 572 Context for the vcs operation when running a merge.
573 573 """
574 574 extras = {
575 575 'ip': '127.0.0.1',
576 576 'username': user_regular.username,
577 577 'user_id': user_regular.user_id,
578 578 'action': 'push',
579 579 'repository': 'fake_target_repo_name',
580 580 'scm': 'git',
581 581 'config': 'fake_config_ini_path',
582 582 'repo_store': '',
583 583 'make_lock': None,
584 584 'locked_by': [None, None, None],
585 585 'server_url': 'http://test.example.com:5000',
586 586 'hooks': ['push', 'pull'],
587 587 'is_shadow_repo': False,
588 588 }
589 589 return extras
590 590
591 591
592 592 @pytest.mark.usefixtures('config_stub')
593 593 class TestUpdateCommentHandling(object):
594 594
595 595 @pytest.fixture(autouse=True, scope='class')
596 596 def enable_outdated_comments(self, request, baseapp):
597 597 config_patch = mock.patch.dict(
598 598 'rhodecode.CONFIG', {'rhodecode_use_outdated_comments': True})
599 599 config_patch.start()
600 600
601 601 @request.addfinalizer
602 602 def cleanup():
603 603 config_patch.stop()
604 604
605 605 def test_comment_stays_unflagged_on_unchanged_diff(self, pr_util):
606 606 commits = [
607 607 {'message': 'a'},
608 608 {'message': 'b', 'added': [FileNode(b'file_b', b'test_content\n')]},
609 609 {'message': 'c', 'added': [FileNode(b'file_c', b'test_content\n')]},
610 610 ]
611 611 pull_request = pr_util.create_pull_request(
612 612 commits=commits, target_head='a', source_head='b', revisions=['b'])
613 613 pr_util.create_inline_comment(file_path='file_b')
614 614 pr_util.add_one_commit(head='c')
615 615
616 616 assert_inline_comments(pull_request, visible=1, outdated=0)
617 617
618 618 def test_comment_stays_unflagged_on_change_above(self, pr_util):
619 619 original_content = b''.join((b'line %d\n' % x for x in range(1, 11)))
620 620 updated_content = b'new_line_at_top\n' + original_content
621 621 commits = [
622 622 {'message': 'a'},
623 623 {'message': 'b', 'added': [FileNode(b'file_b', original_content)]},
624 624 {'message': 'c', 'changed': [FileNode(b'file_b', updated_content)]},
625 625 ]
626 626 pull_request = pr_util.create_pull_request(
627 627 commits=commits, target_head='a', source_head='b', revisions=['b'])
628 628
629 629 with outdated_comments_patcher():
630 630 comment = pr_util.create_inline_comment(
631 631 line_no=u'n8', file_path='file_b')
632 632 pr_util.add_one_commit(head='c')
633 633
634 634 assert_inline_comments(pull_request, visible=1, outdated=0)
635 635 assert comment.line_no == u'n9'
636 636
637 637 def test_comment_stays_unflagged_on_change_below(self, pr_util):
638 638 original_content = b''.join([b'line %d\n' % x for x in range(10)])
639 639 updated_content = original_content + b'new_line_at_end\n'
640 640 commits = [
641 641 {'message': 'a'},
642 642 {'message': 'b', 'added': [FileNode(b'file_b', original_content)]},
643 643 {'message': 'c', 'changed': [FileNode(b'file_b', updated_content)]},
644 644 ]
645 645 pull_request = pr_util.create_pull_request(
646 646 commits=commits, target_head='a', source_head='b', revisions=['b'])
647 647 pr_util.create_inline_comment(file_path='file_b')
648 648 pr_util.add_one_commit(head='c')
649 649
650 650 assert_inline_comments(pull_request, visible=1, outdated=0)
651 651
652 652 @pytest.mark.parametrize('line_no', ['n4', 'o4', 'n10', 'o9'])
653 653 def test_comment_flagged_on_change_around_context(self, pr_util, line_no):
654 654 base_lines = [b'line %d\n' % x for x in range(1, 13)]
655 655 change_lines = list(base_lines)
656 656 change_lines.insert(6, b'line 6a added\n')
657 657
658 658 # Changes on the last line of sight
659 659 update_lines = list(change_lines)
660 660 update_lines[0] = b'line 1 changed\n'
661 661 update_lines[-1] = b'line 12 changed\n'
662 662
663 663 def file_b(lines):
664 664 return FileNode(b'file_b', b''.join(lines))
665 665
666 666 commits = [
667 667 {'message': 'a', 'added': [file_b(base_lines)]},
668 668 {'message': 'b', 'changed': [file_b(change_lines)]},
669 669 {'message': 'c', 'changed': [file_b(update_lines)]},
670 670 ]
671 671
672 672 pull_request = pr_util.create_pull_request(
673 673 commits=commits, target_head='a', source_head='b', revisions=['b'])
674 674 pr_util.create_inline_comment(line_no=line_no, file_path='file_b')
675 675
676 676 with outdated_comments_patcher():
677 677 pr_util.add_one_commit(head='c')
678 678 assert_inline_comments(pull_request, visible=0, outdated=1)
679 679
680 680 @pytest.mark.parametrize("change, content", [
681 681 ('changed', b'changed\n'),
682 682 ('removed', b''),
683 683 ], ids=['changed', b'removed'])
684 684 def test_comment_flagged_on_change(self, pr_util, change, content):
685 685 commits = [
686 686 {'message': 'a'},
687 687 {'message': 'b', 'added': [FileNode(b'file_b', b'test_content\n')]},
688 688 {'message': 'c', change: [FileNode(b'file_b', content)]},
689 689 ]
690 690 pull_request = pr_util.create_pull_request(
691 691 commits=commits, target_head='a', source_head='b', revisions=['b'])
692 692 pr_util.create_inline_comment(file_path='file_b')
693 693
694 694 with outdated_comments_patcher():
695 695 pr_util.add_one_commit(head='c')
696 696 assert_inline_comments(pull_request, visible=0, outdated=1)
697 697
698 698
699 699 @pytest.mark.usefixtures('config_stub')
700 700 class TestUpdateChangedFiles(object):
701 701
702 702 def test_no_changes_on_unchanged_diff(self, pr_util):
703 703 commits = [
704 704 {'message': 'a'},
705 705 {'message': 'b',
706 706 'added': [FileNode(b'file_b', b'test_content b\n')]},
707 707 {'message': 'c',
708 708 'added': [FileNode(b'file_c', b'test_content c\n')]},
709 709 ]
710 710 # open a PR from a to b, adding file_b
711 711 pull_request = pr_util.create_pull_request(
712 712 commits=commits, target_head='a', source_head='b', revisions=['b'],
713 713 name_suffix='per-file-review')
714 714
715 715 # modify PR adding new file file_c
716 716 pr_util.add_one_commit(head='c')
717 717
718 718 assert_pr_file_changes(
719 719 pull_request,
720 720 added=['file_c'],
721 721 modified=[],
722 722 removed=[])
723 723
724 724 def test_modify_and_undo_modification_diff(self, pr_util):
725 725 commits = [
726 726 {'message': 'a'},
727 727 {'message': 'b',
728 728 'added': [FileNode(b'file_b', b'test_content b\n')]},
729 729 {'message': 'c',
730 730 'changed': [FileNode(b'file_b', b'test_content b modified\n')]},
731 731 {'message': 'd',
732 732 'changed': [FileNode(b'file_b', b'test_content b\n')]},
733 733 ]
734 734 # open a PR from a to b, adding file_b
735 735 pull_request = pr_util.create_pull_request(
736 736 commits=commits, target_head='a', source_head='b', revisions=['b'],
737 737 name_suffix='per-file-review')
738 738
739 739 # modify PR modifying file file_b
740 740 pr_util.add_one_commit(head='c')
741 741
742 742 assert_pr_file_changes(
743 743 pull_request,
744 744 added=[],
745 745 modified=['file_b'],
746 746 removed=[])
747 747
748 748 # move the head again to d, which rollbacks change,
749 749 # meaning we should indicate no changes
750 750 pr_util.add_one_commit(head='d')
751 751
752 752 assert_pr_file_changes(
753 753 pull_request,
754 754 added=[],
755 755 modified=[],
756 756 removed=[])
757 757
758 758 def test_updated_all_files_in_pr(self, pr_util):
759 759 commits = [
760 760 {'message': 'a'},
761 761 {'message': 'b', 'added': [
762 762 FileNode(b'file_a', b'test_content a\n'),
763 763 FileNode(b'file_b', b'test_content b\n'),
764 764 FileNode(b'file_c', b'test_content c\n')]},
765 765 {'message': 'c', 'changed': [
766 766 FileNode(b'file_a', b'test_content a changed\n'),
767 767 FileNode(b'file_b', b'test_content b changed\n'),
768 768 FileNode(b'file_c', b'test_content c changed\n')]},
769 769 ]
770 770 # open a PR from a to b, changing 3 files
771 771 pull_request = pr_util.create_pull_request(
772 772 commits=commits, target_head='a', source_head='b', revisions=['b'],
773 773 name_suffix='per-file-review')
774 774
775 775 pr_util.add_one_commit(head='c')
776 776
777 777 assert_pr_file_changes(
778 778 pull_request,
779 779 added=[],
780 780 modified=['file_a', 'file_b', 'file_c'],
781 781 removed=[])
782 782
783 783 def test_updated_and_removed_all_files_in_pr(self, pr_util):
784 784 commits = [
785 785 {'message': 'a'},
786 786 {'message': 'b', 'added': [
787 787 FileNode(b'file_a', b'test_content a\n'),
788 788 FileNode(b'file_b', b'test_content b\n'),
789 789 FileNode(b'file_c', b'test_content c\n')]},
790 790 {'message': 'c', 'removed': [
791 791 FileNode(b'file_a', b'test_content a changed\n'),
792 792 FileNode(b'file_b', b'test_content b changed\n'),
793 793 FileNode(b'file_c', b'test_content c changed\n')]},
794 794 ]
795 795 # open a PR from a to b, removing 3 files
796 796 pull_request = pr_util.create_pull_request(
797 797 commits=commits, target_head='a', source_head='b', revisions=['b'],
798 798 name_suffix='per-file-review')
799 799
800 800 pr_util.add_one_commit(head='c')
801 801
802 802 assert_pr_file_changes(
803 803 pull_request,
804 804 added=[],
805 805 modified=[],
806 806 removed=['file_a', 'file_b', 'file_c'])
807 807
808 808
809 809 def test_update_writes_snapshot_into_pull_request_version(pr_util, config_stub):
810 810 model = PullRequestModel()
811 811 pull_request = pr_util.create_pull_request()
812 812 pr_util.update_source_repository()
813 813
814 814 model.update_commits(pull_request, pull_request.author)
815 815
816 816 # Expect that it has a version entry now
817 817 assert len(model.get_versions(pull_request)) == 1
818 818
819 819
820 820 def test_update_skips_new_version_if_unchanged(pr_util, config_stub):
821 821 pull_request = pr_util.create_pull_request()
822 822 model = PullRequestModel()
823 823 model.update_commits(pull_request, pull_request.author)
824 824
825 825 # Expect that it still has no versions
826 826 assert len(model.get_versions(pull_request)) == 0
827 827
828 828
829 829 def test_update_assigns_comments_to_the_new_version(pr_util, config_stub):
830 830 model = PullRequestModel()
831 831 pull_request = pr_util.create_pull_request()
832 832 comment = pr_util.create_comment()
833 833 pr_util.update_source_repository()
834 834
835 835 model.update_commits(pull_request, pull_request.author)
836 836
837 837 # Expect that the comment is linked to the pr version now
838 838 assert comment.pull_request_version == model.get_versions(pull_request)[0]
839 839
840 840
841 841 def test_update_adds_a_comment_to_the_pull_request_about_the_change(pr_util, config_stub):
842 842 model = PullRequestModel()
843 843 pull_request = pr_util.create_pull_request()
844 844 pr_util.update_source_repository()
845 845 pr_util.update_source_repository()
846 846
847 847 update_response = model.update_commits(pull_request, pull_request.author)
848 848
849 849 commit_id = update_response.common_ancestor_id
850 850 # Expect to find a new comment about the change
851 851 expected_message = textwrap.dedent(
852 852 """\
853 853 Pull request updated. Auto status change to |under_review|
854 854
855 855 .. role:: added
856 856 .. role:: removed
857 857 .. parsed-literal::
858 858
859 859 Changed commits:
860 860 * :added:`1 added`
861 861 * :removed:`0 removed`
862 862
863 863 Changed files:
864 864 * `A file_2 <#a_c-{}-92ed3b5f07b4>`_
865 865
866 866 .. |under_review| replace:: *"Under Review"*"""
867 867 ).format(commit_id[:12])
868 868 pull_request_comments = sorted(
869 869 pull_request.comments, key=lambda c: c.modified_at)
870 870 update_comment = pull_request_comments[-1]
871 871 assert update_comment.text == expected_message
872 872
873 873
874 874 def test_create_version_from_snapshot_updates_attributes(pr_util, config_stub):
875 875 pull_request = pr_util.create_pull_request()
876 876
877 877 # Avoiding default values
878 878 pull_request.status = PullRequest.STATUS_CLOSED
879 879 pull_request._last_merge_source_rev = "0" * 40
880 880 pull_request._last_merge_target_rev = "1" * 40
881 881 pull_request.last_merge_status = 1
882 882 pull_request.merge_rev = "2" * 40
883 883
884 884 # Remember automatic values
885 885 created_on = pull_request.created_on
886 886 updated_on = pull_request.updated_on
887 887
888 888 # Create a new version of the pull request
889 889 version = PullRequestModel()._create_version_from_snapshot(pull_request)
890 890
891 891 # Check attributes
892 892 assert version.title == pr_util.create_parameters['title']
893 893 assert version.description == pr_util.create_parameters['description']
894 894 assert version.status == PullRequest.STATUS_CLOSED
895 895
896 896 # versions get updated created_on
897 897 assert version.created_on != created_on
898 898
899 899 assert version.updated_on == updated_on
900 900 assert version.user_id == pull_request.user_id
901 901 assert version.revisions == pr_util.create_parameters['revisions']
902 902 assert version.source_repo == pr_util.source_repository
903 903 assert version.source_ref == pr_util.create_parameters['source_ref']
904 904 assert version.target_repo == pr_util.target_repository
905 905 assert version.target_ref == pr_util.create_parameters['target_ref']
906 906 assert version._last_merge_source_rev == pull_request._last_merge_source_rev
907 907 assert version._last_merge_target_rev == pull_request._last_merge_target_rev
908 908 assert version.last_merge_status == pull_request.last_merge_status
909 909 assert version.merge_rev == pull_request.merge_rev
910 910 assert version.pull_request == pull_request
911 911
912 912
913 913 def test_link_comments_to_version_only_updates_unlinked_comments(pr_util, config_stub):
914 914 version1 = pr_util.create_version_of_pull_request()
915 915 comment_linked = pr_util.create_comment(linked_to=version1)
916 916 comment_unlinked = pr_util.create_comment()
917 917 version2 = pr_util.create_version_of_pull_request()
918 918
919 919 PullRequestModel()._link_comments_to_version(version2)
920 920 Session().commit()
921 921
922 922 # Expect that only the new comment is linked to version2
923 923 assert (
924 924 comment_unlinked.pull_request_version_id ==
925 925 version2.pull_request_version_id)
926 926 assert (
927 927 comment_linked.pull_request_version_id ==
928 928 version1.pull_request_version_id)
929 929 assert (
930 930 comment_unlinked.pull_request_version_id !=
931 931 comment_linked.pull_request_version_id)
932 932
933 933
934 934 def test_calculate_commits():
935 935 old_ids = [1, 2, 3]
936 936 new_ids = [1, 3, 4, 5]
937 937 change = PullRequestModel()._calculate_commit_id_changes(old_ids, new_ids)
938 938 assert change.added == [4, 5]
939 939 assert change.common == [1, 3]
940 940 assert change.removed == [2]
941 941 assert change.total == [1, 3, 4, 5]
942 942
943 943
944 944 def assert_inline_comments(pull_request, visible=None, outdated=None):
945 945 if visible is not None:
946 946 inline_comments = CommentsModel().get_inline_comments(
947 947 pull_request.target_repo.repo_id, pull_request=pull_request)
948 948 inline_cnt = len(CommentsModel().get_inline_comments_as_list(
949 949 inline_comments))
950 950 assert inline_cnt == visible
951 951 if outdated is not None:
952 952 outdated_comments = CommentsModel().get_outdated_comments(
953 953 pull_request.target_repo.repo_id, pull_request)
954 954 assert len(outdated_comments) == outdated
955 955
956 956
957 957 def assert_pr_file_changes(
958 958 pull_request, added=None, modified=None, removed=None):
959 959 pr_versions = PullRequestModel().get_versions(pull_request)
960 960 # always use first version, ie original PR to calculate changes
961 961 pull_request_version = pr_versions[0]
962 962 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
963 963 pull_request, pull_request_version)
964 964 file_changes = PullRequestModel()._calculate_file_changes(
965 965 old_diff_data, new_diff_data)
966 966
967 967 assert added == file_changes.added, \
968 968 'expected added:%s vs value:%s' % (added, file_changes.added)
969 969 assert modified == file_changes.modified, \
970 970 'expected modified:%s vs value:%s' % (modified, file_changes.modified)
971 971 assert removed == file_changes.removed, \
972 972 'expected removed:%s vs value:%s' % (removed, file_changes.removed)
973 973
974 974
975 975 def outdated_comments_patcher(use_outdated=True):
976 976 return mock.patch.object(
977 977 CommentsModel, 'use_outdated_comments',
978 978 return_value=use_outdated)
@@ -1,834 +1,881 b''
1 1
2 2 ; #########################################
3 3 ; RHODECODE COMMUNITY EDITION CONFIGURATION
4 4 ; #########################################
5 5
6 6 [DEFAULT]
7 7 ; Debug flag sets all loggers to debug, and enables request tracking
8 8 debug = true
9 9
10 10 ; ########################################################################
11 11 ; EMAIL CONFIGURATION
12 12 ; These settings will be used by the RhodeCode mailing system
13 13 ; ########################################################################
14 14
15 15 ; prefix all emails subjects with given prefix, helps filtering out emails
16 16 #email_prefix = [RhodeCode]
17 17
18 18 ; email FROM address all mails will be sent
19 19 #app_email_from = rhodecode-noreply@localhost
20 20
21 21 #smtp_server = mail.server.com
22 22 #smtp_username =
23 23 #smtp_password =
24 24 #smtp_port =
25 25 #smtp_use_tls = false
26 26 #smtp_use_ssl = true
27 27
28 28 [server:main]
29 29 ; COMMON HOST/IP CONFIG, This applies mostly to develop setup,
30 30 ; Host port for gunicorn are controlled by gunicorn_conf.py
31 31 host = 127.0.0.1
32 32 port = 10020
33 33
34 34
35 35 ; ###########################
36 36 ; GUNICORN APPLICATION SERVER
37 37 ; ###########################
38 38
39 ; run with gunicorn --paste rhodecode.ini --config gunicorn_conf.py
39 ; run with gunicorn --config gunicorn_conf.py --paste rhodecode.ini
40 40
41 41 ; Module to use, this setting shouldn't be changed
42 42 use = egg:gunicorn#main
43 43
44 44 ; Prefix middleware for RhodeCode.
45 45 ; recommended when using proxy setup.
46 46 ; allows to set RhodeCode under a prefix in server.
47 47 ; eg https://server.com/custom_prefix. Enable `filter-with =` option below as well.
48 48 ; And set your prefix like: `prefix = /custom_prefix`
49 49 ; be sure to also set beaker.session.cookie_path = /custom_prefix if you need
50 50 ; to make your cookies only work on prefix url
51 51 [filter:proxy-prefix]
52 52 use = egg:PasteDeploy#prefix
53 53 prefix = /
54 54
55 55 [app:main]
56 56 ; The %(here)s variable will be replaced with the absolute path of parent directory
57 57 ; of this file
58 58 ; Each option in the app:main can be override by an environmental variable
59 59 ;
60 60 ;To override an option:
61 61 ;
62 62 ;RC_<KeyName>
63 63 ;Everything should be uppercase, . and - should be replaced by _.
64 64 ;For example, if you have these configuration settings:
65 65 ;rc_cache.repo_object.backend = foo
66 66 ;can be overridden by
67 67 ;export RC_CACHE_REPO_OBJECT_BACKEND=foo
68 68
69 69 use = egg:rhodecode-enterprise-ce
70 70
71 71 ; enable proxy prefix middleware, defined above
72 72 #filter-with = proxy-prefix
73 73
74 74 ; encryption key used to encrypt social plugin tokens,
75 75 ; remote_urls with credentials etc, if not set it defaults to
76 76 ; `beaker.session.secret`
77 77 #rhodecode.encrypted_values.secret =
78 78
79 79 ; decryption strict mode (enabled by default). It controls if decryption raises
80 80 ; `SignatureVerificationError` in case of wrong key, or damaged encryption data.
81 81 #rhodecode.encrypted_values.strict = false
82 82
83 83 ; Pick algorithm for encryption. Either fernet (more secure) or aes (default)
84 84 ; fernet is safer, and we strongly recommend switching to it.
85 85 ; Due to backward compatibility aes is used as default.
86 86 #rhodecode.encrypted_values.algorithm = fernet
87 87
88 88 ; Return gzipped responses from RhodeCode (static files/application)
89 89 gzip_responses = false
90 90
91 91 ; Auto-generate javascript routes file on startup
92 92 generate_js_files = false
93 93
94 94 ; System global default language.
95 95 ; All available languages: en (default), be, de, es, fr, it, ja, pl, pt, ru, zh
96 96 lang = en
97 97
98 98 ; Perform a full repository scan and import on each server start.
99 99 ; Settings this to true could lead to very long startup time.
100 100 startup.import_repos = true
101 101
102 102 ; URL at which the application is running. This is used for Bootstrapping
103 103 ; requests in context when no web request is available. Used in ishell, or
104 104 ; SSH calls. Set this for events to receive proper url for SSH calls.
105 105 app.base_url = http://rhodecode.local
106 106
107 107 ; Host at which the Service API is running.
108 108 app.service_api.host = http://rhodecode.local:10020
109 109
110 110 ; Secret for Service API authentication.
111 111 app.service_api.token =
112 112
113 113 ; Unique application ID. Should be a random unique string for security.
114 114 app_instance_uuid = rc-production
115 115
116 116 ; Cut off limit for large diffs (size in bytes). If overall diff size on
117 117 ; commit, or pull request exceeds this limit this diff will be displayed
118 118 ; partially. E.g 512000 == 512Kb
119 119 cut_off_limit_diff = 1024000
120 120
121 121 ; Cut off limit for large files inside diffs (size in bytes). Each individual
122 122 ; file inside diff which exceeds this limit will be displayed partially.
123 123 ; E.g 128000 == 128Kb
124 124 cut_off_limit_file = 256000
125 125
126 126 ; Use cached version of vcs repositories everywhere. Recommended to be `true`
127 127 vcs_full_cache = false
128 128
129 129 ; Force https in RhodeCode, fixes https redirects, assumes it's always https.
130 130 ; Normally this is controlled by proper flags sent from http server such as Nginx or Apache
131 131 force_https = false
132 132
133 133 ; use Strict-Transport-Security headers
134 134 use_htsts = false
135 135
136 136 ; Set to true if your repos are exposed using the dumb protocol
137 137 git_update_server_info = false
138 138
139 139 ; RSS/ATOM feed options
140 140 rss_cut_off_limit = 256000
141 141 rss_items_per_page = 10
142 142 rss_include_diff = false
143 143
144 144 ; gist URL alias, used to create nicer urls for gist. This should be an
145 145 ; url that does rewrites to _admin/gists/{gistid}.
146 146 ; example: http://gist.rhodecode.org/{gistid}. Empty means use the internal
147 147 ; RhodeCode url, ie. http[s]://rhodecode.server/_admin/gists/{gistid}
148 148 gist_alias_url =
149 149
150 150 ; List of views (using glob pattern syntax) that AUTH TOKENS could be
151 151 ; used for access.
152 152 ; Adding ?auth_token=TOKEN_HASH to the url authenticates this request as if it
153 153 ; came from the the logged in user who own this authentication token.
154 154 ; Additionally @TOKEN syntax can be used to bound the view to specific
155 155 ; authentication token. Such view would be only accessible when used together
156 156 ; with this authentication token
157 157 ; list of all views can be found under `/_admin/permissions/auth_token_access`
158 158 ; The list should be "," separated and on a single line.
159 159 ; Most common views to enable:
160 160
161 161 # RepoCommitsView:repo_commit_download
162 162 # RepoCommitsView:repo_commit_patch
163 163 # RepoCommitsView:repo_commit_raw
164 164 # RepoCommitsView:repo_commit_raw@TOKEN
165 165 # RepoFilesView:repo_files_diff
166 166 # RepoFilesView:repo_archivefile
167 167 # RepoFilesView:repo_file_raw
168 168 # GistView:*
169 169 api_access_controllers_whitelist =
170 170
171 171 ; Default encoding used to convert from and to unicode
172 172 ; can be also a comma separated list of encoding in case of mixed encodings
173 173 default_encoding = UTF-8
174 174
175 175 ; instance-id prefix
176 176 ; a prefix key for this instance used for cache invalidation when running
177 177 ; multiple instances of RhodeCode, make sure it's globally unique for
178 178 ; all running RhodeCode instances. Leave empty if you don't use it
179 179 instance_id =
180 180
181 181 ; Fallback authentication plugin. Set this to a plugin ID to force the usage
182 182 ; of an authentication plugin also if it is disabled by it's settings.
183 183 ; This could be useful if you are unable to log in to the system due to broken
184 184 ; authentication settings. Then you can enable e.g. the internal RhodeCode auth
185 185 ; module to log in again and fix the settings.
186 186 ; Available builtin plugin IDs (hash is part of the ID):
187 187 ; egg:rhodecode-enterprise-ce#rhodecode
188 188 ; egg:rhodecode-enterprise-ce#pam
189 189 ; egg:rhodecode-enterprise-ce#ldap
190 190 ; egg:rhodecode-enterprise-ce#jasig_cas
191 191 ; egg:rhodecode-enterprise-ce#headers
192 192 ; egg:rhodecode-enterprise-ce#crowd
193 193
194 194 #rhodecode.auth_plugin_fallback = egg:rhodecode-enterprise-ce#rhodecode
195 195
196 196 ; Flag to control loading of legacy plugins in py:/path format
197 197 auth_plugin.import_legacy_plugins = true
198 198
199 199 ; alternative return HTTP header for failed authentication. Default HTTP
200 200 ; response is 401 HTTPUnauthorized. Currently HG clients have troubles with
201 201 ; handling that causing a series of failed authentication calls.
202 202 ; Set this variable to 403 to return HTTPForbidden, or any other HTTP code
203 203 ; This will be served instead of default 401 on bad authentication
204 204 auth_ret_code =
205 205
206 206 ; use special detection method when serving auth_ret_code, instead of serving
207 207 ; ret_code directly, use 401 initially (Which triggers credentials prompt)
208 208 ; and then serve auth_ret_code to clients
209 209 auth_ret_code_detection = false
210 210
211 211 ; locking return code. When repository is locked return this HTTP code. 2XX
212 212 ; codes don't break the transactions while 4XX codes do
213 213 lock_ret_code = 423
214 214
215 215 ; Filesystem location were repositories should be stored
216 216 repo_store.path = /var/opt/rhodecode_repo_store
217 217
218 218 ; allows to setup custom hooks in settings page
219 219 allow_custom_hooks_settings = true
220 220
221 221 ; Generated license token required for EE edition license.
222 222 ; New generated token value can be found in Admin > settings > license page.
223 223 license_token = abra-cada-bra1-rce3
224 224
225 225 ; This flag hides sensitive information on the license page such as token, and license data
226 226 license.hide_license_info = false
227 227
228 228 ; supervisor connection uri, for managing supervisor and logs.
229 229 supervisor.uri =
230 230
231 231 ; supervisord group name/id we only want this RC instance to handle
232 232 supervisor.group_id = dev
233 233
234 234 ; Display extended labs settings
235 235 labs_settings_active = true
236 236
237 237 ; Custom exception store path, defaults to TMPDIR
238 238 ; This is used to store exception from RhodeCode in shared directory
239 239 #exception_tracker.store_path =
240 240
241 241 ; Send email with exception details when it happens
242 242 #exception_tracker.send_email = false
243 243
244 244 ; Comma separated list of recipients for exception emails,
245 245 ; e.g admin@rhodecode.com,devops@rhodecode.com
246 246 ; Can be left empty, then emails will be sent to ALL super-admins
247 247 #exception_tracker.send_email_recipients =
248 248
249 249 ; optional prefix to Add to email Subject
250 250 #exception_tracker.email_prefix = [RHODECODE ERROR]
251 251
252 ; File store configuration. This is used to store and serve uploaded files
253 file_store.enabled = true
252 ; NOTE: this setting IS DEPRECATED:
253 ; file_store backend is always enabled
254 #file_store.enabled = true
254 255
256 ; NOTE: this setting IS DEPRECATED:
257 ; file_store.backend = X -> use `file_store.backend.type = filesystem_v2` instead
255 258 ; Storage backend, available options are: local
256 file_store.backend = local
259 #file_store.backend = local
257 260
261 ; NOTE: this setting IS DEPRECATED:
262 ; file_store.storage_path = X -> use `file_store.filesystem_v2.storage_path = X` instead
258 263 ; path to store the uploaded binaries and artifacts
259 file_store.storage_path = /var/opt/rhodecode_data/file_store
264 #file_store.storage_path = /var/opt/rhodecode_data/file_store
265
266 ; Artifacts file-store, is used to store comment attachments and artifacts uploads.
267 ; file_store backend type: filesystem_v1, filesystem_v2 or objectstore (s3-based) are available as options
268 ; filesystem_v1 is backwards compat with pre 5.1 storage changes
269 ; new installations should choose filesystem_v2 or objectstore (s3-based), pick filesystem when migrating from
270 ; previous installations to keep the artifacts without a need of migration
271 file_store.backend.type = filesystem_v1
272
273 ; filesystem options...
274 file_store.filesystem_v1.storage_path = /var/opt/rhodecode_data/test_artifacts_file_store
275
276 ; filesystem_v2 options...
277 file_store.filesystem_v2.storage_path = /var/opt/rhodecode_data/test_artifacts_file_store_2
278 file_store.filesystem_v2.shards = 8
260 279
280 ; objectstore options...
281 ; url for s3 compatible storage that allows to upload artifacts
282 ; e.g http://minio:9000
283 #file_store.backend.type = objectstore
284 file_store.objectstore.url = http://s3-minio:9000
285
286 ; a top-level bucket to put all other shards in
287 ; objects will be stored in rhodecode-file-store/shard-N based on the bucket_shards number
288 file_store.objectstore.bucket = rhodecode-file-store-tests
289
290 ; number of sharded buckets to create to distribute archives across
291 ; default is 8 shards
292 file_store.objectstore.bucket_shards = 8
293
294 ; key for s3 auth
295 file_store.objectstore.key = s3admin
296
297 ; secret for s3 auth
298 file_store.objectstore.secret = s3secret4
299
300 ;region for s3 storage
301 file_store.objectstore.region = eu-central-1
261 302
262 303 ; Redis url to acquire/check generation of archives locks
263 304 archive_cache.locking.url = redis://redis:6379/1
264 305
265 306 ; Storage backend, only 'filesystem' and 'objectstore' are available now
266 307 archive_cache.backend.type = filesystem
267 308
268 309 ; url for s3 compatible storage that allows to upload artifacts
269 310 ; e.g http://minio:9000
270 311 archive_cache.objectstore.url = http://s3-minio:9000
271 312
272 313 ; key for s3 auth
273 314 archive_cache.objectstore.key = key
274 315
275 316 ; secret for s3 auth
276 317 archive_cache.objectstore.secret = secret
277 318
278 319 ;region for s3 storage
279 320 archive_cache.objectstore.region = eu-central-1
280 321
281 322 ; number of sharded buckets to create to distribute archives across
282 323 ; default is 8 shards
283 324 archive_cache.objectstore.bucket_shards = 8
284 325
285 326 ; a top-level bucket to put all other shards in
286 327 ; objects will be stored in rhodecode-archive-cache/shard-N based on the bucket_shards number
287 328 archive_cache.objectstore.bucket = rhodecode-archive-cache
288 329
289 330 ; if true, this cache will try to retry with retry_attempts=N times waiting retry_backoff time
290 331 archive_cache.objectstore.retry = false
291 332
292 333 ; number of seconds to wait for next try using retry
293 334 archive_cache.objectstore.retry_backoff = 1
294 335
295 336 ; how many tries do do a retry fetch from this backend
296 337 archive_cache.objectstore.retry_attempts = 10
297 338
298 339 ; Default is $cache_dir/archive_cache if not set
299 340 ; Generated repo archives will be cached at this location
300 341 ; and served from the cache during subsequent requests for the same archive of
301 342 ; the repository. This path is important to be shared across filesystems and with
302 343 ; RhodeCode and vcsserver
303 344 archive_cache.filesystem.store_dir = %(here)s/rc-tests/archive_cache
304 345
305 346 ; The limit in GB sets how much data we cache before recycling last used, defaults to 10 gb
306 347 archive_cache.filesystem.cache_size_gb = 2
307 348
308 349 ; Eviction policy used to clear out after cache_size_gb limit is reached
309 350 archive_cache.filesystem.eviction_policy = least-recently-stored
310 351
311 352 ; By default cache uses sharding technique, this specifies how many shards are there
312 353 ; default is 8 shards
313 354 archive_cache.filesystem.cache_shards = 8
314 355
315 356 ; if true, this cache will try to retry with retry_attempts=N times waiting retry_backoff time
316 357 archive_cache.filesystem.retry = false
317 358
318 359 ; number of seconds to wait for next try using retry
319 360 archive_cache.filesystem.retry_backoff = 1
320 361
321 362 ; how many tries do do a retry fetch from this backend
322 363 archive_cache.filesystem.retry_attempts = 10
323 364
324 365
325 366 ; #############
326 367 ; CELERY CONFIG
327 368 ; #############
328 369
329 370 ; manually run celery: /path/to/celery worker --task-events --beat --app rhodecode.lib.celerylib.loader --scheduler rhodecode.lib.celerylib.scheduler.RcScheduler --loglevel DEBUG --ini /path/to/rhodecode.ini
330 371
331 372 use_celery = false
332 373
333 374 ; path to store schedule database
334 375 #celerybeat-schedule.path =
335 376
336 377 ; connection url to the message broker (default redis)
337 378 celery.broker_url = redis://redis:6379/8
338 379
339 380 ; results backend to get results for (default redis)
340 381 celery.result_backend = redis://redis:6379/8
341 382
342 383 ; rabbitmq example
343 384 #celery.broker_url = amqp://rabbitmq:qweqwe@localhost:5672/rabbitmqhost
344 385
345 386 ; maximum tasks to execute before worker restart
346 387 celery.max_tasks_per_child = 20
347 388
348 389 ; tasks will never be sent to the queue, but executed locally instead.
349 390 celery.task_always_eager = true
350 391 celery.task_store_eager_result = true
351 392
352 393 ; #############
353 394 ; DOGPILE CACHE
354 395 ; #############
355 396
356 397 ; Default cache dir for caches. Putting this into a ramdisk can boost performance.
357 398 ; eg. /tmpfs/data_ramdisk, however this directory might require large amount of space
358 399 cache_dir = %(here)s/rc-test-data
359 400
360 401 ; *********************************************
361 402 ; `sql_cache_short` cache for heavy SQL queries
362 403 ; Only supported backend is `memory_lru`
363 404 ; *********************************************
364 405 rc_cache.sql_cache_short.backend = dogpile.cache.rc.memory_lru
365 406 rc_cache.sql_cache_short.expiration_time = 0
366 407
367 408
368 409 ; *****************************************************
369 410 ; `cache_repo_longterm` cache for repo object instances
370 411 ; Only supported backend is `memory_lru`
371 412 ; *****************************************************
372 413 rc_cache.cache_repo_longterm.backend = dogpile.cache.rc.memory_lru
373 414 ; by default we use 30 Days, cache is still invalidated on push
374 415 rc_cache.cache_repo_longterm.expiration_time = 2592000
375 416 ; max items in LRU cache, set to smaller number to save memory, and expire last used caches
376 417 rc_cache.cache_repo_longterm.max_size = 10000
377 418
378 419
379 420 ; *********************************************
380 421 ; `cache_general` cache for general purpose use
381 422 ; for simplicity use rc.file_namespace backend,
382 423 ; for performance and scale use rc.redis
383 424 ; *********************************************
384 425 rc_cache.cache_general.backend = dogpile.cache.rc.file_namespace
385 426 rc_cache.cache_general.expiration_time = 43200
386 427 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set
387 428 rc_cache.cache_general.arguments.filename = %(here)s/rc-tests/cache-backend/cache_general_db
388 429
389 430 ; alternative `cache_general` redis backend with distributed lock
390 431 #rc_cache.cache_general.backend = dogpile.cache.rc.redis
391 432 #rc_cache.cache_general.expiration_time = 300
392 433
393 434 ; redis_expiration_time needs to be greater then expiration_time
394 435 #rc_cache.cache_general.arguments.redis_expiration_time = 7200
395 436
396 437 #rc_cache.cache_general.arguments.host = localhost
397 438 #rc_cache.cache_general.arguments.port = 6379
398 439 #rc_cache.cache_general.arguments.db = 0
399 440 #rc_cache.cache_general.arguments.socket_timeout = 30
400 441 ; more Redis options: https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends
401 442 #rc_cache.cache_general.arguments.distributed_lock = true
402 443
403 444 ; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen
404 445 #rc_cache.cache_general.arguments.lock_auto_renewal = true
405 446
406 447 ; *************************************************
407 448 ; `cache_perms` cache for permission tree, auth TTL
408 449 ; for simplicity use rc.file_namespace backend,
409 450 ; for performance and scale use rc.redis
410 451 ; *************************************************
411 452 rc_cache.cache_perms.backend = dogpile.cache.rc.file_namespace
412 453 rc_cache.cache_perms.expiration_time = 0
413 454 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set
414 455 rc_cache.cache_perms.arguments.filename = %(here)s/rc-tests/cache-backend/cache_perms_db
415 456
416 457 ; alternative `cache_perms` redis backend with distributed lock
417 458 #rc_cache.cache_perms.backend = dogpile.cache.rc.redis
418 459 #rc_cache.cache_perms.expiration_time = 300
419 460
420 461 ; redis_expiration_time needs to be greater then expiration_time
421 462 #rc_cache.cache_perms.arguments.redis_expiration_time = 7200
422 463
423 464 #rc_cache.cache_perms.arguments.host = localhost
424 465 #rc_cache.cache_perms.arguments.port = 6379
425 466 #rc_cache.cache_perms.arguments.db = 0
426 467 #rc_cache.cache_perms.arguments.socket_timeout = 30
427 468 ; more Redis options: https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends
428 469 #rc_cache.cache_perms.arguments.distributed_lock = true
429 470
430 471 ; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen
431 472 #rc_cache.cache_perms.arguments.lock_auto_renewal = true
432 473
433 474 ; ***************************************************
434 475 ; `cache_repo` cache for file tree, Readme, RSS FEEDS
435 476 ; for simplicity use rc.file_namespace backend,
436 477 ; for performance and scale use rc.redis
437 478 ; ***************************************************
438 479 rc_cache.cache_repo.backend = dogpile.cache.rc.file_namespace
439 480 rc_cache.cache_repo.expiration_time = 2592000
440 481 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set
441 482 rc_cache.cache_repo.arguments.filename = %(here)s/rc-tests/cache-backend/cache_repo_db
442 483
443 484 ; alternative `cache_repo` redis backend with distributed lock
444 485 #rc_cache.cache_repo.backend = dogpile.cache.rc.redis
445 486 #rc_cache.cache_repo.expiration_time = 2592000
446 487
447 488 ; redis_expiration_time needs to be greater then expiration_time
448 489 #rc_cache.cache_repo.arguments.redis_expiration_time = 2678400
449 490
450 491 #rc_cache.cache_repo.arguments.host = localhost
451 492 #rc_cache.cache_repo.arguments.port = 6379
452 493 #rc_cache.cache_repo.arguments.db = 1
453 494 #rc_cache.cache_repo.arguments.socket_timeout = 30
454 495 ; more Redis options: https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends
455 496 #rc_cache.cache_repo.arguments.distributed_lock = true
456 497
457 498 ; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen
458 499 #rc_cache.cache_repo.arguments.lock_auto_renewal = true
459 500
460 501 ; ##############
461 502 ; BEAKER SESSION
462 503 ; ##############
463 504
464 505 ; beaker.session.type is type of storage options for the logged users sessions. Current allowed
465 506 ; types are file, ext:redis, ext:database, ext:memcached
466 507 ; Fastest ones are ext:redis and ext:database, DO NOT use memory type for session
467 508 beaker.session.type = file
468 509 beaker.session.data_dir = %(here)s/rc-tests/data/sessions
469 510
470 511 ; Redis based sessions
471 512 #beaker.session.type = ext:redis
472 513 #beaker.session.url = redis://redis:6379/2
473 514
474 515 ; DB based session, fast, and allows easy management over logged in users
475 516 #beaker.session.type = ext:database
476 517 #beaker.session.table_name = db_session
477 518 #beaker.session.sa.url = postgresql://postgres:secret@localhost/rhodecode
478 519 #beaker.session.sa.url = mysql://root:secret@127.0.0.1/rhodecode
479 520 #beaker.session.sa.pool_recycle = 3600
480 521 #beaker.session.sa.echo = false
481 522
482 523 beaker.session.key = rhodecode
483 524 beaker.session.secret = test-rc-uytcxaz
484 525 beaker.session.lock_dir = %(here)s/rc-tests/data/sessions/lock
485 526
486 527 ; Secure encrypted cookie. Requires AES and AES python libraries
487 528 ; you must disable beaker.session.secret to use this
488 529 #beaker.session.encrypt_key = key_for_encryption
489 530 #beaker.session.validate_key = validation_key
490 531
491 532 ; Sets session as invalid (also logging out user) if it haven not been
492 533 ; accessed for given amount of time in seconds
493 534 beaker.session.timeout = 2592000
494 535 beaker.session.httponly = true
495 536
496 537 ; Path to use for the cookie. Set to prefix if you use prefix middleware
497 538 #beaker.session.cookie_path = /custom_prefix
498 539
499 540 ; Set https secure cookie
500 541 beaker.session.secure = false
501 542
502 543 ; default cookie expiration time in seconds, set to `true` to set expire
503 544 ; at browser close
504 545 #beaker.session.cookie_expires = 3600
505 546
506 547 ; #############################
507 548 ; SEARCH INDEXING CONFIGURATION
508 549 ; #############################
509 550
510 551 ; Full text search indexer is available in rhodecode-tools under
511 552 ; `rhodecode-tools index` command
512 553
513 554 ; WHOOSH Backend, doesn't require additional services to run
514 555 ; it works good with few dozen repos
515 556 search.module = rhodecode.lib.index.whoosh
516 557 search.location = %(here)s/rc-tests/data/index
517 558
518 559 ; ####################
519 560 ; CHANNELSTREAM CONFIG
520 561 ; ####################
521 562
522 563 ; channelstream enables persistent connections and live notification
523 564 ; in the system. It's also used by the chat system
524 565
525 566 channelstream.enabled = false
526 567
527 568 ; server address for channelstream server on the backend
528 569 channelstream.server = channelstream:9800
529 570
530 571 ; location of the channelstream server from outside world
531 572 ; use ws:// for http or wss:// for https. This address needs to be handled
532 573 ; by external HTTP server such as Nginx or Apache
533 574 ; see Nginx/Apache configuration examples in our docs
534 575 channelstream.ws_url = ws://rhodecode.yourserver.com/_channelstream
535 576 channelstream.secret = ENV_GENERATED
536 577 channelstream.history.location = %(here)s/rc-tests/channelstream_history
537 578
538 579 ; Internal application path that Javascript uses to connect into.
539 580 ; If you use proxy-prefix the prefix should be added before /_channelstream
540 581 channelstream.proxy_path = /_channelstream
541 582
542 583
543 584 ; ##############################
544 585 ; MAIN RHODECODE DATABASE CONFIG
545 586 ; ##############################
546 587
547 588 #sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db?timeout=30
548 589 #sqlalchemy.db1.url = postgresql://postgres:qweqwe@localhost/rhodecode
549 590 #sqlalchemy.db1.url = mysql://root:qweqwe@localhost/rhodecode?charset=utf8
550 591 ; pymysql is an alternative driver for MySQL, use in case of problems with default one
551 592 #sqlalchemy.db1.url = mysql+pymysql://root:qweqwe@localhost/rhodecode
552 593
553 594 sqlalchemy.db1.url = sqlite:///%(here)s/rc-tests/rhodecode_test.db?timeout=30
554 595
555 596 ; see sqlalchemy docs for other advanced settings
556 597 ; print the sql statements to output
557 598 sqlalchemy.db1.echo = false
558 599
559 600 ; recycle the connections after this amount of seconds
560 601 sqlalchemy.db1.pool_recycle = 3600
561 602
562 603 ; the number of connections to keep open inside the connection pool.
563 604 ; 0 indicates no limit
564 605 ; the general calculus with gevent is:
565 606 ; if your system allows 500 concurrent greenlets (max_connections) that all do database access,
566 607 ; then increase pool size + max overflow so that they add up to 500.
567 608 #sqlalchemy.db1.pool_size = 5
568 609
569 610 ; The number of connections to allow in connection pool "overflow", that is
570 611 ; connections that can be opened above and beyond the pool_size setting,
571 612 ; which defaults to five.
572 613 #sqlalchemy.db1.max_overflow = 10
573 614
574 615 ; Connection check ping, used to detect broken database connections
575 616 ; could be enabled to better handle cases if MySQL has gone away errors
576 617 #sqlalchemy.db1.ping_connection = true
577 618
578 619 ; ##########
579 620 ; VCS CONFIG
580 621 ; ##########
581 622 vcs.server.enable = true
582 623 vcs.server = vcsserver:10010
583 624
584 625 ; Web server connectivity protocol, responsible for web based VCS operations
585 626 ; Available protocols are:
586 627 ; `http` - use http-rpc backend (default)
587 628 vcs.server.protocol = http
588 629
589 630 ; Push/Pull operations protocol, available options are:
590 631 ; `http` - use http-rpc backend (default)
591 632 vcs.scm_app_implementation = http
592 633
593 634 ; Push/Pull operations hooks protocol, available options are:
594 635 ; `http` - use http-rpc backend (default)
595 636 ; `celery` - use celery based hooks
637 #DEPRECATED:vcs.hooks.protocol = http
596 638 vcs.hooks.protocol = http
597 639
598 640 ; Host on which this instance is listening for hooks. vcsserver will call this host to pull/push hooks so it should be
599 641 ; accessible via network.
600 642 ; Use vcs.hooks.host = "*" to bind to current hostname (for Docker)
601 643 vcs.hooks.host = *
602 644
603 645 ; Start VCSServer with this instance as a subprocess, useful for development
604 646 vcs.start_server = false
605 647
606 648 ; List of enabled VCS backends, available options are:
607 649 ; `hg` - mercurial
608 650 ; `git` - git
609 651 ; `svn` - subversion
610 652 vcs.backends = hg, git, svn
611 653
612 654 ; Wait this number of seconds before killing connection to the vcsserver
613 655 vcs.connection_timeout = 3600
614 656
615 657 ; Cache flag to cache vcsserver remote calls locally
616 658 ; It uses cache_region `cache_repo`
617 659 vcs.methods.cache = false
618 660
619 661 ; ####################################################
620 662 ; Subversion proxy support (mod_dav_svn)
621 663 ; Maps RhodeCode repo groups into SVN paths for Apache
622 664 ; ####################################################
623 665
624 666 ; Compatibility version when creating SVN repositories. Defaults to newest version when commented out.
625 667 ; Set a numeric version for your current SVN e.g 1.8, or 1.12
626 668 ; Legacy available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible, pre-1.9-compatible
627 669 #vcs.svn.compatible_version = 1.8
628 670
671 ; Redis connection settings for svn integrations logic
672 ; This connection string needs to be the same on ce and vcsserver
673 vcs.svn.redis_conn = redis://redis:6379/0
674
629 675 ; Enable SVN proxy of requests over HTTP
630 676 vcs.svn.proxy.enabled = true
631 677
632 678 ; host to connect to running SVN subsystem
633 679 vcs.svn.proxy.host = http://svn:8090
634 680
635 681 ; Enable or disable the config file generation.
636 682 svn.proxy.generate_config = false
637 683
638 684 ; Generate config file with `SVNListParentPath` set to `On`.
639 685 svn.proxy.list_parent_path = true
640 686
641 687 ; Set location and file name of generated config file.
642 688 svn.proxy.config_file_path = %(here)s/rc-tests/mod_dav_svn.conf
643 689
644 690 ; alternative mod_dav config template. This needs to be a valid mako template
645 691 ; Example template can be found in the source code:
646 692 ; rhodecode/apps/svn_support/templates/mod-dav-svn.conf.mako
647 693 #svn.proxy.config_template = ~/.rccontrol/enterprise-1/custom_svn_conf.mako
648 694
649 695 ; Used as a prefix to the `Location` block in the generated config file.
650 696 ; In most cases it should be set to `/`.
651 697 svn.proxy.location_root = /
652 698
653 699 ; Command to reload the mod dav svn configuration on change.
654 700 ; Example: `/etc/init.d/apache2 reload` or /home/USER/apache_reload.sh
655 701 ; Make sure user who runs RhodeCode process is allowed to reload Apache
656 702 #svn.proxy.reload_cmd = /etc/init.d/apache2 reload
657 703
658 704 ; If the timeout expires before the reload command finishes, the command will
659 705 ; be killed. Setting it to zero means no timeout. Defaults to 10 seconds.
660 706 #svn.proxy.reload_timeout = 10
661 707
662 708 ; ####################
663 709 ; SSH Support Settings
664 710 ; ####################
665 711
666 712 ; Defines if a custom authorized_keys file should be created and written on
667 713 ; any change user ssh keys. Setting this to false also disables possibility
668 714 ; of adding SSH keys by users from web interface. Super admins can still
669 715 ; manage SSH Keys.
670 716 ssh.generate_authorized_keyfile = true
671 717
672 718 ; Options for ssh, default is `no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding`
673 719 # ssh.authorized_keys_ssh_opts =
674 720
675 721 ; Path to the authorized_keys file where the generate entries are placed.
676 722 ; It is possible to have multiple key files specified in `sshd_config` e.g.
677 723 ; AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys_rhodecode
678 724 ssh.authorized_keys_file_path = %(here)s/rc-tests/authorized_keys_rhodecode
679 725
680 726 ; Command to execute the SSH wrapper. The binary is available in the
681 727 ; RhodeCode installation directory.
682 728 ; legacy: /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper
683 729 ; new rewrite: /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper-v2
684 ssh.wrapper_cmd = /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper
730 #DEPRECATED: ssh.wrapper_cmd = /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper
731 ssh.wrapper_cmd.v2 = /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper-v2
685 732
686 733 ; Allow shell when executing the ssh-wrapper command
687 734 ssh.wrapper_cmd_allow_shell = false
688 735
689 736 ; Enables logging, and detailed output send back to the client during SSH
690 737 ; operations. Useful for debugging, shouldn't be used in production.
691 738 ssh.enable_debug_logging = true
692 739
693 740 ; Paths to binary executable, by default they are the names, but we can
694 741 ; override them if we want to use a custom one
695 742 ssh.executable.hg = /usr/local/bin/rhodecode_bin/vcs_bin/hg
696 743 ssh.executable.git = /usr/local/bin/rhodecode_bin/vcs_bin/git
697 744 ssh.executable.svn = /usr/local/bin/rhodecode_bin/vcs_bin/svnserve
698 745
699 746 ; Enables SSH key generator web interface. Disabling this still allows users
700 747 ; to add their own keys.
701 748 ssh.enable_ui_key_generator = true
702 749
703 750 ; Statsd client config, this is used to send metrics to statsd
704 751 ; We recommend setting statsd_exported and scrape them using Prometheus
705 752 #statsd.enabled = false
706 753 #statsd.statsd_host = 0.0.0.0
707 754 #statsd.statsd_port = 8125
708 755 #statsd.statsd_prefix =
709 756 #statsd.statsd_ipv6 = false
710 757
711 758 ; configure logging automatically at server startup set to false
712 759 ; to use the below custom logging config.
713 760 ; RC_LOGGING_FORMATTER
714 761 ; RC_LOGGING_LEVEL
715 762 ; env variables can control the settings for logging in case of autoconfigure
716 763
717 764 logging.autoconfigure = false
718 765
719 766 ; specify your own custom logging config file to configure logging
720 767 #logging.logging_conf_file = /path/to/custom_logging.ini
721 768
722 769 ; Dummy marker to add new entries after.
723 770 ; Add any custom entries below. Please don't remove this marker.
724 771 custom.conf = 1
725 772
726 773
727 774 ; #####################
728 775 ; LOGGING CONFIGURATION
729 776 ; #####################
730 777
731 778 [loggers]
732 779 keys = root, sqlalchemy, beaker, celery, rhodecode, ssh_wrapper, dogpile
733 780
734 781 [handlers]
735 782 keys = console, console_sql
736 783
737 784 [formatters]
738 785 keys = generic, json, color_formatter, color_formatter_sql
739 786
740 787 ; #######
741 788 ; LOGGERS
742 789 ; #######
743 790 [logger_root]
744 791 level = NOTSET
745 792 handlers = console
746 793
747 794 [logger_routes]
748 795 level = DEBUG
749 796 handlers =
750 797 qualname = routes.middleware
751 798 ## "level = DEBUG" logs the route matched and routing variables.
752 799 propagate = 1
753 800
754 801 [logger_sqlalchemy]
755 802 level = INFO
756 803 handlers = console_sql
757 804 qualname = sqlalchemy.engine
758 805 propagate = 0
759 806
760 807 [logger_beaker]
761 808 level = DEBUG
762 809 handlers =
763 810 qualname = beaker.container
764 811 propagate = 1
765 812
766 813 [logger_dogpile]
767 814 level = INFO
768 815 handlers = console
769 816 qualname = dogpile
770 817 propagate = 1
771 818
772 819 [logger_rhodecode]
773 820 level = DEBUG
774 821 handlers =
775 822 qualname = rhodecode
776 823 propagate = 1
777 824
778 825 [logger_ssh_wrapper]
779 826 level = DEBUG
780 827 handlers =
781 828 qualname = ssh_wrapper
782 829 propagate = 1
783 830
784 831 [logger_celery]
785 832 level = DEBUG
786 833 handlers =
787 834 qualname = celery
788 835
789 836
790 837 ; ########
791 838 ; HANDLERS
792 839 ; ########
793 840
794 841 [handler_console]
795 842 class = StreamHandler
796 843 args = (sys.stderr, )
797 844 level = DEBUG
798 845 ; To enable JSON formatted logs replace 'generic/color_formatter' with 'json'
799 846 ; This allows sending properly formatted logs to grafana loki or elasticsearch
800 847 formatter = generic
801 848
802 849 [handler_console_sql]
803 850 ; "level = DEBUG" logs SQL queries and results.
804 851 ; "level = INFO" logs SQL queries.
805 852 ; "level = WARN" logs neither. (Recommended for production systems.)
806 853 class = StreamHandler
807 854 args = (sys.stderr, )
808 855 level = WARN
809 856 ; To enable JSON formatted logs replace 'generic/color_formatter_sql' with 'json'
810 857 ; This allows sending properly formatted logs to grafana loki or elasticsearch
811 858 formatter = generic
812 859
813 860 ; ##########
814 861 ; FORMATTERS
815 862 ; ##########
816 863
817 864 [formatter_generic]
818 865 class = rhodecode.lib.logging_formatter.ExceptionAwareFormatter
819 866 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
820 867 datefmt = %Y-%m-%d %H:%M:%S
821 868
822 869 [formatter_color_formatter]
823 870 class = rhodecode.lib.logging_formatter.ColorFormatter
824 871 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
825 872 datefmt = %Y-%m-%d %H:%M:%S
826 873
827 874 [formatter_color_formatter_sql]
828 875 class = rhodecode.lib.logging_formatter.ColorFormatterSql
829 876 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
830 877 datefmt = %Y-%m-%d %H:%M:%S
831 878
832 879 [formatter_json]
833 880 format = %(timestamp)s %(levelname)s %(name)s %(message)s %(req_id)s
834 881 class = rhodecode.lib._vendor.jsonlogger.JsonFormatter
@@ -1,200 +1,201 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 # Import early to make sure things are patched up properly
20 20 from setuptools import setup, find_packages, Extension
21 21
22 22 import os
23 23 import re
24 24 import sys
25 25 import pkgutil
26 26 import platform
27 27 import codecs
28 28
29 29 import pip
30 30
31 31 pip_major_version = int(pip.__version__.split(".")[0])
32 32 if pip_major_version >= 20:
33 33 from pip._internal.req import parse_requirements
34 34 from pip._internal.network.session import PipSession
35 35 elif pip_major_version >= 10:
36 36 from pip._internal.req import parse_requirements
37 37 from pip._internal.download import PipSession
38 38 else:
39 39 from pip.req import parse_requirements
40 40 from pip.download import PipSession
41 41
42 42
43 43 def get_package_name(req_object):
44 44 package_name = None
45 45 try:
46 46 from pip._internal.req.constructors import install_req_from_parsed_requirement
47 47 except ImportError:
48 48 install_req_from_parsed_requirement = None
49 49
50 50 # In 20.1 of pip, the requirements object changed
51 51 if hasattr(req_object, 'req'):
52 52 package_name = req_object.req.name
53 53
54 54 if package_name is None:
55 55 if install_req_from_parsed_requirement:
56 56 package = install_req_from_parsed_requirement(req_object)
57 57 package_name = package.req.name
58 58
59 59 if package_name is None:
60 60 # fallback for older pip
61 61 package_name = re.split('===|<=|!=|==|>=|~=|<|>', req_object.requirement)[0]
62 62
63 63 return package_name
64 64
65 65
66 66 if sys.version_info < (3, 10):
67 67 raise Exception('RhodeCode requires Python 3.10 or later')
68 68
69 69 here = os.path.abspath(os.path.dirname(__file__))
70 70
71 71 # defines current platform
72 72 __platform__ = platform.system()
73 73 __license__ = 'AGPLv3, and Commercial License'
74 74 __author__ = 'RhodeCode GmbH'
75 75 __url__ = 'https://code.rhodecode.com'
76 76 is_windows = __platform__ in ('Windows',)
77 77
78 78
79 79 def _get_requirements(req_filename, exclude=None, extras=None):
80 80 extras = extras or []
81 81 exclude = exclude or []
82 82
83 83 try:
84 84 parsed = parse_requirements(
85 85 os.path.join(here, req_filename), session=PipSession())
86 86 except TypeError:
87 87 # try pip < 6.0.0, that doesn't support session
88 88 parsed = parse_requirements(os.path.join(here, req_filename))
89 89
90 90 requirements = []
91 91 for int_req in parsed:
92 92 req_name = get_package_name(int_req)
93 93 if req_name not in exclude:
94 94 requirements.append(req_name)
95 95 return requirements + extras
96 96
97 97
98 98 # requirements extract
99 99 setup_requirements = ['PasteScript']
100 100 install_requirements = _get_requirements(
101 101 'requirements.txt', exclude=['setuptools', 'entrypoints'])
102 102 test_requirements = _get_requirements(
103 103 'requirements_test.txt')
104 104
105 105
106 106 def get_version():
107 107 here = os.path.abspath(os.path.dirname(__file__))
108 108 ver_file = os.path.join(here, "rhodecode", "VERSION")
109 109 with open(ver_file, "rt") as f:
110 110 version = f.read().strip()
111 111
112 112 return version
113 113
114 114
115 115 # additional files that goes into package itself
116 116 package_data = {
117 117 '': ['*.txt', '*.rst'],
118 118 'configs': ['*.ini'],
119 119 'rhodecode': ['VERSION', 'i18n/*/LC_MESSAGES/*.mo', ],
120 120 }
121 121
122 122 description = 'Source Code Management Platform'
123 123 keywords = ' '.join([
124 124 'rhodecode', 'mercurial', 'git', 'svn',
125 125 'code review',
126 126 'repo groups', 'ldap', 'repository management', 'hgweb',
127 127 'hgwebdir', 'gitweb', 'serving hgweb',
128 128 ])
129 129
130 130
131 131 # README/DESCRIPTION generation
132 132 readme_file = 'README.rst'
133 133 changelog_file = 'CHANGES.rst'
134 134 try:
135 135 long_description = codecs.open(readme_file).read() + '\n\n' + \
136 136 codecs.open(changelog_file).read()
137 137 except IOError as err:
138 138 sys.stderr.write(
139 139 "[WARNING] Cannot find file specified as long_description (%s)\n "
140 140 "or changelog (%s) skipping that file" % (readme_file, changelog_file))
141 141 long_description = description
142 142
143 143
144 144 setup(
145 145 name='rhodecode-enterprise-ce',
146 146 version=get_version(),
147 147 description=description,
148 148 long_description=long_description,
149 149 keywords=keywords,
150 150 license=__license__,
151 151 author=__author__,
152 152 author_email='support@rhodecode.com',
153 153 url=__url__,
154 154 setup_requires=setup_requirements,
155 155 install_requires=install_requirements,
156 156 tests_require=test_requirements,
157 157 zip_safe=False,
158 158 packages=find_packages(exclude=["docs", "tests*"]),
159 159 package_data=package_data,
160 160 include_package_data=True,
161 161 classifiers=[
162 162 'Development Status :: 6 - Mature',
163 163 'Environment :: Web Environment',
164 164 'Intended Audience :: Developers',
165 165 'Operating System :: OS Independent',
166 166 'Topic :: Software Development :: Version Control',
167 167 'License :: OSI Approved :: Affero GNU General Public License v3 or later (AGPLv3+)',
168 168 'Programming Language :: Python :: 3.10',
169 169 ],
170 170 message_extractors={
171 171 'rhodecode': [
172 172 ('**.py', 'python', None),
173 173 ('**.js', 'javascript', None),
174 174 ('templates/**.mako', 'mako', {'input_encoding': 'utf-8'}),
175 175 ('templates/**.html', 'mako', {'input_encoding': 'utf-8'}),
176 176 ('public/**', 'ignore', None),
177 177 ]
178 178 },
179 179
180 180 entry_points={
181 181 'paste.app_factory': [
182 182 'main=rhodecode.config.middleware:make_pyramid_app',
183 183 ],
184 184 'pyramid.pshell_runner': [
185 185 'ipython = rhodecode.lib.pyramid_shell:ipython_shell_runner',
186 186 ],
187 187 'console_scripts': [
188 188 'rc-setup-app=rhodecode.lib.rc_commands.setup_rc:main',
189 189 'rc-upgrade-db=rhodecode.lib.rc_commands.upgrade_db:main',
190 190 'rc-ishell=rhodecode.lib.rc_commands.ishell:main',
191 191 'rc-add-artifact=rhodecode.lib.rc_commands.add_artifact:main',
192 'rc-migrate-artifact=rhodecode.lib.rc_commands.migrate_artifact:main',
192 193 'rc-ssh-wrapper=rhodecode.apps.ssh_support.lib.ssh_wrapper_v1:main',
193 194 'rc-ssh-wrapper-v2=rhodecode.apps.ssh_support.lib.ssh_wrapper_v2:main',
194 195 ],
195 196 'beaker.backends': [
196 197 'memorylru_base=rhodecode.lib.memory_lru_dict:MemoryLRUNamespaceManagerBase',
197 198 'memorylru_debug=rhodecode.lib.memory_lru_dict:MemoryLRUNamespaceManagerDebug'
198 199 ]
199 200 },
200 201 )
@@ -1,1 +0,0 b''
1 Example init scripts. No newline at end of file
@@ -1,61 +0,0 b''
1 ; Sample supervisor RhodeCode config file.
2 ;
3 ; For more information on the config file, please see:
4 ; http://supervisord.org/configuration.html
5 ;
6 ; Note: shell expansion ("~" or "$HOME") is not supported. Environment
7 ; variables can be expanded using this syntax: "%(ENV_HOME)s".
8
9 [unix_http_server]
10 file=/tmp/supervisor.sock ; (the path to the socket file)
11 ;chmod=0700 ; socket file mode (default 0700)
12 ;chown=nobody:nogroup ; socket file uid:gid owner
13 ;username=user ; (default is no username (open server))
14 ;password=123 ; (default is no password (open server))
15
16 [supervisord]
17 logfile=/home/ubuntu/rhodecode/supervisord.log ; (main log file;default $CWD/supervisord.log)
18 logfile_maxbytes=50MB ; (max main logfile bytes b4 rotation;default 50MB)
19 logfile_backups=10 ; (num of main logfile rotation backups;default 10)
20 loglevel=info ; (log level;default info; others: debug,warn,trace)
21 pidfile=/home/ubuntu/rhodecode/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
22 nodaemon=true ; (start in foreground if true;default false)
23 minfds=1024 ; (min. avail startup file descriptors;default 1024)
24 minprocs=200 ; (min. avail process descriptors;default 200)
25 ;umask=022 ; (process file creation umask;default 022)
26 user=ubuntu ; (default is current user, required if root)
27 ;identifier=supervisor ; (supervisord identifier, default is 'supervisor')
28 ;directory=/tmp ; (default is not to cd during start)
29 ;nocleanup=true ; (don't clean up tempfiles at start;default false)
30 ;childlogdir=/tmp ; ('AUTO' child log dir, default $TEMP)
31 environment=HOME=/home/ubuntu,LANG=en_US.UTF-8 ; (key value pairs to add to environment)
32 ;strip_ansi=false ; (strip ansi escape codes in logs; def. false)
33
34 ; the below section must remain in the config file for RPC
35 ; (supervisorctl/web interface) to work, additional interfaces may be
36 ; added by defining them in separate rpcinterface: sections
37 [rpcinterface:supervisor]
38 supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
39
40 [supervisorctl]
41 serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket
42 ;username=chris ; should be same as http_username if set
43 ;password=123 ; should be same as http_password if set
44
45
46 ; restart with supervisorctl restart rhodecode:*
47 [program:rhodecode]
48 numprocs = 1
49 numprocs_start = 5000
50 directory=/home/ubuntu/rhodecode/source
51 command = /home/ubuntu/rhodecode/venv/bin/paster serve /home/ubuntu/rhodecode/source/prod.ini
52 process_name = %(program_name)s_%(process_num)04d
53 redirect_stderr=true
54 stdout_logfile=/home/ubuntu/rhodecode/rhodecode.log
55
56 [program:rhodecode_workers]
57 numproces = 1
58 directory = /home/ubuntu/rhodecode/source
59 command = /home/ubuntu/rhodecode/venv/bin/paster celeryd /home/ubuntu/rhodecode/source/prod.ini --autoscale=10,2
60 redirect_stderr=true
61 stdout_logfile=/%(here)s/rhodecode_workers.log
@@ -1,268 +0,0 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 import os
20 import time
21 import errno
22 import hashlib
23
24 from rhodecode.lib.ext_json import json
25 from rhodecode.apps.file_store import utils
26 from rhodecode.apps.file_store.extensions import resolve_extensions
27 from rhodecode.apps.file_store.exceptions import (
28 FileNotAllowedException, FileOverSizeException)
29
30 METADATA_VER = 'v1'
31
32
33 def safe_make_dirs(dir_path):
34 if not os.path.exists(dir_path):
35 try:
36 os.makedirs(dir_path)
37 except OSError as e:
38 if e.errno != errno.EEXIST:
39 raise
40 return
41
42
43 class LocalFileStorage(object):
44
45 @classmethod
46 def apply_counter(cls, counter, filename):
47 name_counted = '%d-%s' % (counter, filename)
48 return name_counted
49
50 @classmethod
51 def resolve_name(cls, name, directory):
52 """
53 Resolves a unique name and the correct path. If a filename
54 for that path already exists then a numeric prefix with values > 0 will be
55 added, for example test.jpg -> 1-test.jpg etc. initially file would have 0 prefix.
56
57 :param name: base name of file
58 :param directory: absolute directory path
59 """
60
61 counter = 0
62 while True:
63 name_counted = cls.apply_counter(counter, name)
64
65 # sub_store prefix to optimize disk usage, e.g some_path/ab/final_file
66 sub_store = cls._sub_store_from_filename(name_counted)
67 sub_store_path = os.path.join(directory, sub_store)
68 safe_make_dirs(sub_store_path)
69
70 path = os.path.join(sub_store_path, name_counted)
71 if not os.path.exists(path):
72 return name_counted, path
73 counter += 1
74
75 @classmethod
76 def _sub_store_from_filename(cls, filename):
77 return filename[:2]
78
79 @classmethod
80 def calculate_path_hash(cls, file_path):
81 """
82 Efficient calculation of file_path sha256 sum
83
84 :param file_path:
85 :return: sha256sum
86 """
87 digest = hashlib.sha256()
88 with open(file_path, 'rb') as f:
89 for chunk in iter(lambda: f.read(1024 * 100), b""):
90 digest.update(chunk)
91
92 return digest.hexdigest()
93
94 def __init__(self, base_path, extension_groups=None):
95
96 """
97 Local file storage
98
99 :param base_path: the absolute base path where uploads are stored
100 :param extension_groups: extensions string
101 """
102
103 extension_groups = extension_groups or ['any']
104 self.base_path = base_path
105 self.extensions = resolve_extensions([], groups=extension_groups)
106
107 def __repr__(self):
108 return f'{self.__class__}@{self.base_path}'
109
110 def store_path(self, filename):
111 """
112 Returns absolute file path of the filename, joined to the
113 base_path.
114
115 :param filename: base name of file
116 """
117 prefix_dir = ''
118 if '/' in filename:
119 prefix_dir, filename = filename.split('/')
120 sub_store = self._sub_store_from_filename(filename)
121 else:
122 sub_store = self._sub_store_from_filename(filename)
123 return os.path.join(self.base_path, prefix_dir, sub_store, filename)
124
125 def delete(self, filename):
126 """
127 Deletes the filename. Filename is resolved with the
128 absolute path based on base_path. If file does not exist,
129 returns **False**, otherwise **True**
130
131 :param filename: base name of file
132 """
133 if self.exists(filename):
134 os.remove(self.store_path(filename))
135 return True
136 return False
137
138 def exists(self, filename):
139 """
140 Checks if file exists. Resolves filename's absolute
141 path based on base_path.
142
143 :param filename: file_uid name of file, e.g 0-f62b2b2d-9708-4079-a071-ec3f958448d4.svg
144 """
145 return os.path.exists(self.store_path(filename))
146
147 def filename_allowed(self, filename, extensions=None):
148 """Checks if a filename has an allowed extension
149
150 :param filename: base name of file
151 :param extensions: iterable of extensions (or self.extensions)
152 """
153 _, ext = os.path.splitext(filename)
154 return self.extension_allowed(ext, extensions)
155
156 def extension_allowed(self, ext, extensions=None):
157 """
158 Checks if an extension is permitted. Both e.g. ".jpg" and
159 "jpg" can be passed in. Extension lookup is case-insensitive.
160
161 :param ext: extension to check
162 :param extensions: iterable of extensions to validate against (or self.extensions)
163 """
164 def normalize_ext(_ext):
165 if _ext.startswith('.'):
166 _ext = _ext[1:]
167 return _ext.lower()
168
169 extensions = extensions or self.extensions
170 if not extensions:
171 return True
172
173 ext = normalize_ext(ext)
174
175 return ext in [normalize_ext(x) for x in extensions]
176
177 def save_file(self, file_obj, filename, directory=None, extensions=None,
178 extra_metadata=None, max_filesize=None, randomized_name=True, **kwargs):
179 """
180 Saves a file object to the uploads location.
181 Returns the resolved filename, i.e. the directory +
182 the (randomized/incremented) base name.
183
184 :param file_obj: **cgi.FieldStorage** object (or similar)
185 :param filename: original filename
186 :param directory: relative path of sub-directory
187 :param extensions: iterable of allowed extensions, if not default
188 :param max_filesize: maximum size of file that should be allowed
189 :param randomized_name: generate random generated UID or fixed based on the filename
190 :param extra_metadata: extra JSON metadata to store next to the file with .meta suffix
191
192 """
193
194 extensions = extensions or self.extensions
195
196 if not self.filename_allowed(filename, extensions):
197 raise FileNotAllowedException()
198
199 if directory:
200 dest_directory = os.path.join(self.base_path, directory)
201 else:
202 dest_directory = self.base_path
203
204 safe_make_dirs(dest_directory)
205
206 uid_filename = utils.uid_filename(filename, randomized=randomized_name)
207
208 # resolve also produces special sub-dir for file optimized store
209 filename, path = self.resolve_name(uid_filename, dest_directory)
210 stored_file_dir = os.path.dirname(path)
211
212 no_body_seek = kwargs.pop('no_body_seek', False)
213 if no_body_seek:
214 pass
215 else:
216 file_obj.seek(0)
217
218 with open(path, "wb") as dest:
219 length = 256 * 1024
220 while 1:
221 buf = file_obj.read(length)
222 if not buf:
223 break
224 dest.write(buf)
225
226 metadata = {}
227 if extra_metadata:
228 metadata = extra_metadata
229
230 size = os.stat(path).st_size
231
232 if max_filesize and size > max_filesize:
233 # free up the copied file, and raise exc
234 os.remove(path)
235 raise FileOverSizeException()
236
237 file_hash = self.calculate_path_hash(path)
238
239 metadata.update({
240 "filename": filename,
241 "size": size,
242 "time": time.time(),
243 "sha256": file_hash,
244 "meta_ver": METADATA_VER
245 })
246
247 filename_meta = filename + '.meta'
248 with open(os.path.join(stored_file_dir, filename_meta), "wb") as dest_meta:
249 dest_meta.write(json.dumps(metadata))
250
251 if directory:
252 filename = os.path.join(directory, filename)
253
254 return filename, metadata
255
256 def get_metadata(self, filename, ignore_missing=False):
257 """
258 Reads JSON stored metadata for a file
259
260 :param filename:
261 :return:
262 """
263 filename = self.store_path(filename)
264 filename_meta = filename + '.meta'
265 if ignore_missing and not os.path.isfile(filename_meta):
266 return {}
267 with open(filename_meta, "rb") as source_meta:
268 return json.loads(source_meta.read())
General Comments 0
You need to be logged in to leave comments. Login now